A password reset is a login that skips the password, so every control on the login form has to be matched or deliberately replaced there. If the reset path is weaker than the login path, an attacker will use it. The OWASP Forgot Password Cheat Sheet calls the function "a common source of vulnerabilities", with user enumeration as the best-known example.
Why the reset flow is the weak point
A successful login produces a session. According to the OWASP session management guidance, an authenticated session ID is temporarily equivalent to the strongest authentication method the application used. A reset that ends in a session, or in a password the attacker chose, hands over the same thing.
NIST SP 800-63B-4 requires proof of possession and control of two distinct authentication factors at AAL2. If the login asks for two factors, a reset that needs one emailed token is a single-factor way around it. The excerpt we rely on does not cover recovery; the full document has a section on authenticator event management.
Our own rule: on an account with an enrolled second factor, ask for that factor or a saved recovery code before the new password is accepted, and never let a reset remove or replace the factor.
The request step
Answer identically whether or not the account exists. "If that address is registered, we have sent an email" is enough.
Timing leaks too. A handler that exits early for unknown addresses and sends mail for known ones shows the difference whatever the message says. Run the same logic for every request, or hand the mail to an asynchronous job.
Add per-account rate-limiting, a CAPTCHA or a similar control. Without one, an attacker can fire thousands of reset requests per hour at one account and bury the victim's inbox or SMS. Do not lock or change the account on request.
The token
OWASP asks four things of a token or code:
- it is generated with a cryptographically safe random algorithm;
- it is long enough to defeat brute force;
- it is stored securely;
- it is single use and expires after an appropriate period.
The sheet gives no figures for length or lifetime. The nearest number in the sources is in the session guidance: a CSPRNG with at least 128 bits of entropy, 160 bits or more preferred. It is written for session tokens; we apply it to reset tokens. For lifetime, choose a short value, write it down and test against it.
"Stored securely" can be read strictly. If read-only disclosure of the token store is in your threat model, the session guidance describes an identifier and secret verifier split. The server keeps the public lookup identifier and the full SHA-256 hash of the verifier, and compares hashes in constant time. The identifier alone must never count as proof. Applying this to reset tokens is our suggestion.
Deliver the token through a side channel, email or SMS, never in the browser's response. A token in the URL is the simplest design, but the session guidance warns that IDs in URLs can leak through links and logs, browser history and bookmarks, the Referer header and search engines. Keeping third-party content and outbound links off the reset page closes the Referer leak only. For the rest, exchange the token on the first request for short-lived server-side state, redirect to a clean URL, and keep that first query string out of access logs.
The reset step
- Have the user type the new password twice, and check it against the same policy as registration.
- Hash it through the same code path as every other password.
- Invalidate every outstanding reset token for the account when the password or the email address changes. Otherwise an older mail still sitting in an inbox can reset the account again.
- Email the user to say the password changed. Never put the password in the message.
- Send the user to the normal login instead of signing them in. Automatic login adds session-handling code, and with it the chance of new vulnerabilities.
- End existing sessions, or at least offer to.
After a suspected compromise, the attacker's live session survives a password change unless you end it. We would make invalidation the default.
MFA and compromised accounts
A password reset alone may not restore control. The attacker may still hold an active session, or may have changed recovery information or MFA methods.
OWASP says not to rely solely on recently added or changed recovery information. Use independent evidence established earlier, such as saved recovery codes, in a combination that meets the account's assurance requirements. Resetting MFA has its own section in OWASP's Multifactor Authentication Cheat Sheet, so keep that process separate from the password flow.
The OWASP Authentication Cheat Sheet also covers changing a registered email address, which matters because a changed address redirects every future reset.
Testing the flow
| Test | What a pass looks like |
|---|---|
| Real and non-existent address | Same message and status code |
| Scripted batch of requests per address | Median response times match |
| Repeated requests for one account | Rate limit or CAPTCHA triggers |
| Request a reset, then log in | Account not locked or altered |
| Token used twice, or after expiry | Attempt fails |
| Old token after a password or email change | Attempt fails |
| Reset page with the token in the URL | No third-party requests or outbound links |
| Reset with an old session open | Old session ended |
| Enrolled second factor, emailed token only (our rule) | Reset refused |
| Final response and the mail | No automatic login, no password in the mail |
For the timing row, compare medians and spread across many runs. This belongs in any penetration test of an authentication flow.
Where to start
- Request a reset for an existing and a non-existent account many times each. Compare message, status code and response time.
- Read the token generator. Confirm it uses a CSPRNG, make tokens single-use, and set a lifetime you can state in minutes.
- Add per-account rate-limiting or a CAPTCHA to the request endpoint, and confirm a request never locks or modifies the account.
- Make the reset end other sessions, invalidate outstanding tokens, and send a notification email without the password. Check that the reset page loads nothing from third parties.
- Take a test account with MFA enrolled and try to reset it using only the emailed token. Confirm the reset is refused.
- Check who can reset or remove a second factor, what evidence they must present, and whether recovery treats recently changed details as untrusted.
