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:
- How is this request authenticated? If it is a bearer token in a header, there is no CSRF here and never will be. Stop.
- What is the session cookie's
SameSitevalue? 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
Phase 0 in practice
# 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:
| Credential | CSRF-able? | Note |
|---|---|---|
| Session cookie | Yes | The normal case. SameSite applies. |
| JWT stored in a cookie | Yes | It is a cookie. "We use JWT" is not a defence when the JWT is ambient. |
| HTTP Basic / Digest | Yes | Ambient, and SameSite does not apply. |
| NTLM / Negotiate | Yes | Ambient. Intranet applications are wide open. |
| TLS client certificate | Yes | Ambient. |
Authorization: Bearer from JS | No | Not ambient. The browser never adds it. |
| API key in a custom header | No | Same reasoning, and the header would preflight. |
| API key in a query parameter | No for CSRF | The 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:
- 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".
- The cookie posture, quoted from the response.
Set-Cookie: session=...; Path=/; HttpOnly; Securewith a note that there is noSameSiteattribute. - 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.
- The specific defensive gap, named. "The token is not bound to the session" rather than "CSRF protection is weak".
- A minimal PoC, self-contained HTML the triager can save and open. Generate it on the PoC page.
- Reproduction steps including which account was logged in and what to check afterwards to confirm the effect.
- 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=Strictblocks 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.
Related
What still works and what died with Lax-by-default — the honest, browser-by-browser exploitability verdict, plus the techniques that are now purely historical.
Token not checked, checked only when present, empty accepted, not tied to the session, reused across users, or predictable.
Burp's CSRF PoC generator and where it produces a 2026-invalid PoC, ZAP, why curl proves nothing, and browser devtools.
The browser attaches your credentials to any request an attacker can cause. The server sees a valid session and cannot tell who asked for it.
Exactly when Lax, Strict, None, and an absent attribute attach a cookie to a cross-site request — and why the three engines disagree.
The layering order — SameSite=Lax, __Host- prefix, Fetch Metadata rejection, then a token — plus re-authentication for high-value operations.