Skip to content
CWE-352A01:2021 – Broken Access Control

Methodology

Enumerate, classify by auth mechanism, read the SameSite value first, then test — and write the finding with the browsers named.

Read the cookie before you write a PoC

The instinctive order — find a form, strip the token, build a PoC — wastes time in 2026, because it defers the two questions that decide whether anything is exploitable at all:

  1. How is this request authenticated? If it is a bearer token in a header, there is no CSRF here and never will be. Stop.
  2. What is the session cookie's SameSite value? This determines which delivery shapes can work, and therefore which PoC is worth building.

Both are answered by reading one response. Doing that first will often tell you that the endpoint is unreachable from a cross-site page regardless of what the token check does — or that a SameSite=None cookie means everything is on the table.

The order below reflects that. It is arranged so each step eliminates the most possibilities per minute spent.

The sequence

PhaseWhat to doWhat it decides
0. Cookie postureRead every Set-Cookie. Note SameSite, Secure, HttpOnly, Domain, and any __Host- prefix on the session and any CSRF cookie.Which rows of the cheatsheet are live. None means old rules apply; absent means a browser-dependent finding; Strict means you need a same-site gadget first.
1. Auth mechanismDetermine what actually authenticates each endpoint: session cookie, Basic/NTLM, client certificate, or a bearer token.Whether CSRF applies at all. Bearer means no. Basic and client certificates mean SameSite is irrelevant and everything is live.
2. Enumerate state changesCrawl and list every endpoint that writes. Prioritise the ones granting durable control: email, password, recovery phone, API keys, SSH keys, OAuth links, team membership, admin promotion.The target list, ordered by impact.
3. Check the defences presentFor each endpoint: is there a token? An Origin or Sec-Fetch-Site check? A required custom header? An enforced content type? Re-authentication?Which bypass guide applies.
4. Test the checksWork the list in token-validation-flaws: remove the token, empty it, alter it, use your own account's, switch the method, switch the content type.Whether a gap exists. One replayed request per test.
5. Build the PoCPick the delivery that matches the cookie posture from phase 0. Generate it, host it, open it in a real browser.Confirmation. A proxy replay is not proof.
6. Verify per browserTest in Chrome AND Firefox at minimum. They will disagree whenever the cookie has no SameSite attribute.What you can honestly claim in the report.

Phase 0 in practice

HTTP
# Log in and read every Set-Cookie in the response. This is the single most
# informative request in the whole assessment.

HTTP/1.1 302 Found
Set-Cookie: session=eyJ1c2VyIjoi...; Path=/; HttpOnly; Secure
Set-Cookie: csrf=8f3a2b...; Path=/; Secure; Domain=.example.com

# Read it line by line:
#
#   session  -- NO SameSite attribute.
#              -> Chromium treats it as Lax: cross-site POST blocked, except
#                 within 120s of it being set.
#              -> Firefox and Safari send it on a top-level POST.
#              -> So: a top-level form POST is a FIREFOX AND SAFARI finding.
#              -> And a top-level GET works everywhere.
#
#   csrf     -- Domain=.example.com, and not HttpOnly.
#              -> Every subdomain receives it AND can overwrite it.
#              -> If this is double-submit, check whether the token is signed.
#                 If it is a bare random value, cookie tossing defeats it.
#              -> No __Host- prefix, so there is nothing stopping a sibling.
#
# Also check on a later request whether the session cookie is RE-ISSUED on
# every response. If it is, it is permanently under 120 seconds old and the
# Chromium Lax+POST window is permanently open.

Phase 1: classify honestly

This is where most false positives are born. Be strict:

CredentialCSRF-able?Note
Session cookieYesThe normal case. SameSite applies.
JWT stored in a cookieYesIt is a cookie. "We use JWT" is not a defence when the JWT is ambient.
HTTP Basic / DigestYesAmbient, and SameSite does not apply.
NTLM / NegotiateYesAmbient. Intranet applications are wide open.
TLS client certificateYesAmbient.
Authorization: Bearer from JSNoNot ambient. The browser never adds it.
API key in a custom headerNoSame reasoning, and the header would preflight.
API key in a query parameterNo for CSRFThe attacker would need the key, at which point they do not need the victim.

If an application uses a bearer token for its API and a cookie for its web session, test each surface separately — the same action may be CSRF-able on one and not the other.

Writing the finding

A CSRF report that does not name the browser is incomplete, and a triager who tests in the wrong one will close it. Include, in this order:

  1. The impact of the action, concretely. "An attacker can change the victim's account email and then use password reset to take over the account" — not "CSRF was found".
  2. The cookie posture, quoted from the response. Set-Cookie: session=...; Path=/; HttpOnly; Secure with a note that there is no SameSite attribute.
  3. Which browsers it works in, and why. "Verified in Firefox 141 and Safari 18. Blocked in Chrome by the Lax-by-default policy, except within 120 seconds of login." This pre-empts the most common rejection.
  4. The specific defensive gap, named. "The token is not bound to the session" rather than "CSRF protection is weak".
  5. A minimal PoC, self-contained HTML the triager can save and open. Generate it on the PoC page.
  6. Reproduction steps including which account was logged in and what to check afterwards to confirm the effect.
  7. The fix, specific to their framework — see the Cheatsheet and the prevention guides.

If the attack only works in a minority browser, say so plainly and rate it accordingly. Overstating reach is the fastest way to lose credibility on an otherwise good finding.

Common false positives

Worth checking against before submitting:

  • "It worked in curl." Of course it did — you set every header yourself. Only a browser test proves a browser would send it.
  • A missing token on a bearer-authenticated endpoint. Not CSRF.
  • Logout CSRF alone. Low to informational unless it is a step in a chain.
  • A missing token where SameSite=Strict blocks every delivery. Report it as a defence-in-depth observation, not as an exploitable finding, unless you found a same-site gadget.
  • "The token changes on every page load." That is BREACH masking in Django, Rails, and Spring. Both values are valid.
  • A CSRF finding on a page that also has XSS. XSS defeats every CSRF defence; report the XSS.
  • Access-Control-Allow-Origin: *. Incompatible with credentials — the browser refuses the combination. Only a finding if the endpoint returns sensitive data without needing credentials.
  • A self-CSRF where the attacker must already know the victim's credentials to set up the attack.