Intermediate levelWith account

Password reset — Puny code, header injection, email normalization, host header

Password reset bypass with Unicode confusables, Host header injection, email normalization (gmail + alias), token leakage in referer, response manipulation.

Gorka El BochiMay 11, 202613 min

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

arduino
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:

LatinCyrillicCode 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

  1. The victim has account gorka@target.com.
  2. The attacker registers an account with gоrka@target.com (the о is Cyrillic U+043E).
  3. Registration passes because the app treats strings byte-exact → it's a "different" email.
  4. The attacker requests a password reset for the VICTIM gorka@target.com.
  5. 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.

graphql
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

  1. The victim registers usuario@gmail.com on target.com.
  2. The attacker registers us.uario@gmail.com on target.com (Gmail delivers both to the same inbox; target.com treats them as different).
  3. The attacker requests a password reset for usuario@gmail.com.
  4. The email arrives at usuario@gmail.com (the victim).
  5. But if the app normalizes on reset (u.s.uariousuario) and the lookup finds the attacker → the attacker receives the reset → ATO.

Plus aliases

scss
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.

<!-- PAYWALL -->

Host header injection

Perhaps the oldest and still most exploitable bypass. The app builds the reset link using the request's Host header:

javascript
const resetLink = `https://${req.headers.host}/reset?token=${token}`;
sendEmail(user.email, resetLink);

The attacker sends:

http
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:

perl
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 with evil.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:

json
{
  "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:

  1. The victim resets their password with token X → OK.
  2. 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:

json
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:

  1. The attacker registers a new account.
  2. Changes the email to victim@target.com (without verification).
  3. Requests a password reset → email to victim@target.com.
  4. 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-Host and observe the received email.
  • Puny code: register victim@target.com with Cyrillic (NIST homographs).
  • Gmail normalization: try user@gmail.com vs u.ser@gmail.com vs user+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).

Practice Host header injection, Puny code and normalization attacks in password reset labs.

Practice this in a lab

Password Reset

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account
Premium · 1 more technique

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.

Unlock

€7.99/mo · cancel anytime

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy