The “I lost my key” process that opens the door for anyone
Watch it happen
Mallory asks for a reset for Alice. The clerk puts the reset slip in the reply to Mallory, who uses it to set a new PIN.
Mallory walks up to the bank. She does not know Alice’s PIN, and she does not need to.
In bank terms and app terms
| In the bank | In your app |
|---|---|
Are you exposed?
Your forgot-password endpoint returns a token, link or “user found” flag in its JSON response. Reset tokens are short, numeric, sequential or never expire, and can be used more than once. Your reset form answers differently for emails that exist and emails that do not.
Send the token only by email, and respond with the same message for every request. Generate long random tokens (use your auth provider’s built-in reset), expire them in minutes, and make them single-use. Rate limit reset requests per email and per IP so nobody can flood inboxes or guess tokens.
It has happened to real companies
Snapchat (2013–14) — Snapchat's Find Friends feature let phone numbers be matched to usernames, and Snapchat itself warned that attackers could upload huge sets of numbers, "like every number in an area code". At the start of 2014 a group published about 4.6 million usernames matched to phone numbers (last two digits hidden), after the security firm Gibson Security had warned Snapchat about the flaw. Snapchat then promised better rate limiting. Enumeration without limits is the same weakness that makes reset flows leak who has an account.
What it looks like in code
// GOOD: Same response regardless of email validity
return NextResponse.json({ message: "If an account exists, a reset link was sent." })
// Never reveal whether the email exists
// GOOD: Rate limit reset requests
// GOOD: Never return the token in the HTTP response
// Send it via email only:
await sendEmail(email, `Reset: https://app.com/reset?token=${token}`)
return NextResponse.json({ message: "If an account exists, a reset link was sent." })
Ask your AI tool to fix it
Paste this into Cursor, Claude, Lovable, Bolt or whatever you build with.
Review my password reset flow. Make sure the reset token is never returned in any API response and is only sent by email. Use long random single-use tokens that expire within 15 minutes, return the same message whether or not the email exists, store only a hash of the token, invalidate it after use and after a password change, and add rate limiting. Prefer my auth provider’s built-in reset if available.Where this comes from
The facts, figures and fixes on this page were checked against these sources on 25 Oct 2026. They are linked so you can check for yourself.
- Forgot Password Cheat SheetOWASPSame response for every request, random single-use hashed tokens, expiry and rate limiting
- WSTG: Testing for Account Enumeration and Guessable User AccountOWASPReset forms that answer differently for existing emails leak who has an account
- Password-based AuthSupabaseAuth providers ship a built-in reset flow that emails the link to the account owner
- Rate limitsSupabaseProviders rate limit reset and email requests per address and per IP
- The Snapchat leakO'Reilly RadarFind Friends let huge numbers of phone numbers be matched to usernames; 4.6 million leaked
- Snapchat leaks 4.6 million phone numbers, names after ignoring warningsESET WeLiveSecurityGibson Security warned Snapchat first; last two digits of numbers were hidden
Spot a mistake or something out of date? Report a correction · How these guides are made and checked
The free, open-source scanner checks for most of the problems in these guides.