Password Reset Security: Common Flaws in Account Recovery Flows
Account recovery is an edge case in the sense that most users encounter it infrequently. It is not an edge case in the sense of being low risk. A password reset flow that can be exploited is a direct path to account takeover — and because reset flows are built once and rarely revisited, the issues found in them tend to persist for years.
The core of the problem is attention. The primary login flow, password hashing, and session management are subject to regular scrutiny. Password reset receives less of it. The result is a class of vulnerabilities that are relatively simple to identify and remediate but consistently appear in production applications across all sectors.
How Password Reset Flows Work
The standard email-based password reset follows a predictable sequence:
- The user submits their email address or username to the reset endpoint
- The application generates a time-limited, single-use token
- The application sends an email containing a link that includes the token
- The user clicks the link, which validates the token and permits a password change
- The token is invalidated and the password is updated
Each step in this sequence introduces potential failure modes. The quality of the token, the scope of the email response, the construction of the reset link, the validation logic, and the invalidation behavior are each independently testable and independently exploitable when implemented incorrectly.
Token Predictability
The reset token is the authentication credential for the account recovery operation. Its security properties must equal or exceed those of the primary credential it is temporarily replacing.
Tokens generated from insufficient entropy sources are predictable. Common failure patterns include:
Timestamp-derived tokens. Some applications construct tokens from the current timestamp — often millisecond epoch time, optionally combined with the user ID or email address. If an attacker knows that a reset was requested at a specific time (which they may know if they initiated the request themselves or observed server logs), the token space is small enough to enumerate. A millisecond-precision timestamp produces approximately 86 billion possible values per day, but if the request time is known to within a few seconds, the space shrinks to tens of thousands.
Short numeric tokens. Four-digit or six-digit numeric tokens (presented as a code in the email body rather than a URL) are at best a million-value space. Without rate limiting, they are brute-forceable through automated submission. Six-digit numeric codes are standard in time-based OTP systems specifically because they are coupled with rate limiting and short expiry — without those controls, the length is insufficient.
Sequential or predictable identifiers. If the token is or encodes the primary key of a reset request record in the database, and the primary key is a sequential integer, the token space is effectively the total number of reset requests ever generated.
Correct implementation uses a cryptographically secure pseudorandom number generator to produce at least 128 bits of entropy, typically encoded as a hex or base64 string of 32+ characters. The token should not contain or encode any information about the user or the request.
Token Expiry and Invalidation
Even a cryptographically sound token provides inadequate security if it does not expire or persists after use.
Missing or excessive expiry. Tokens should expire within a short window — 15 to 30 minutes is typical, with some implementations using as little as 10. A token valid for 24 hours or longer persists through a period during which the user might log in through their primary credentials, forget about the reset, or move to a different device. An attacker who intercepts the token at any point within that window can use it. Implementations with no expiry at all are effectively issuing permanent secondary credentials.
No invalidation after use. After a successful password reset, the token must be marked invalid. Tokens that remain valid after use — detectable by clicking the reset link a second time after already resetting the password — allow anyone who observes the token after the fact (in browser history, proxy logs, referrer headers, email forwarding chains) to reset the password again.
Old tokens not invalidated when new ones are issued. When a user requests multiple resets in succession, each request should invalidate all previous tokens for that account. Applications that allow multiple active tokens simultaneously create extended windows and complicate the user experience: if a user requests a reset, does not receive the email immediately, and requests again, an attacker who intercepts either token can use it.
Host Header Injection in Reset Links
The password reset email contains a URL linking back to the application. In many implementations, the base URL for this link is constructed dynamically from the incoming HTTP request:
reset_url = f"https://{request.headers['Host']}/reset?token={token}"This is a critical mistake. The Host header is attacker-controlled — nothing in HTTP prevents a client from sending any value in this header. If the application does not validate the incoming Host value against a configured allowlist of permitted domains, an attacker can manipulate the link destination.
The attack requires the ability to send or intercept the password reset request. In a self-service reset flow initiated by the user, the attacker would need to trigger the reset on behalf of a target — which is possible for any public-facing application where the attacker knows or can guess the target's email address.
The attacker sends:
POST /reset-password HTTP/1.1
Host: attacker.com
Content-Type: application/json
{"email": "victim@example.com"}The application generates a valid token tied to the victim's account and constructs:
https://attacker.com/reset?token=<valid_token>
This link is sent to the victim's email. If the victim clicks it, the token value appears in the attacker's server logs, in the URL of the request to the attacker's domain, and potentially in a referrer header when the victim's browser follows any subsequent redirects. The attacker now holds a valid password reset token for the victim's account.
The fix is straightforward: construct the reset URL from a configured base URL, not from the incoming request:
BASE_URL = "https://app.example.com"
reset_url = f"{BASE_URL}/reset?token={token}"Frameworks that expose the request host for URL generation should either validate the Host header against an allowlist or use explicit configuration.
Account Enumeration Through Differential Responses
Password reset endpoints frequently leak account existence information through inconsistent responses.
Applications that respond differently depending on whether the submitted email address matches a registered account produce enumerable signals. The differences appear in several forms:
Response body text. "We've sent a reset link to that address" versus "That email address is not registered" is the most obvious form. Many applications that handle the success case carefully still have a distinct response for the failure case, or vice versa.
HTTP status codes. A 200 response for found accounts and a 404 or 400 for not-found accounts is directly enumerable.
Response timing. Generating a token, creating a database record, and sending an email takes measurably longer than simply returning an error for an unknown address. Even applications with identical response bodies may leak account existence through timing differences of tens or hundreds of milliseconds.
The correct response is to return an identical message regardless of whether the address is registered:
"If an account is associated with this email address, you will receive a reset link shortly."
The response body, status code, and — to the extent feasible — response timing should be consistent across both cases.
Rate Limiting and Brute Force
Password reset endpoints are frequently excluded from rate limiting that applies to the login endpoint, on the assumption that token entropy makes brute force impractical. This assumption depends entirely on the token design.
Short or numeric tokens without rate limiting are directly brute-forceable. A six-digit code combined with an absent rate limit reduces account takeover to a script that submits numeric guesses until one succeeds.
More subtly, even well-designed tokens require rate limiting on the reset request itself. Submitting thousands of reset requests for a user's account is a denial-of-service against their inbox, and the ability to trigger unlimited reset emails is occasionally useful for attackers maintaining access (invalidating all sessions by forcing repeated resets) or for harassment.
Effective rate limiting applies to:
- The number of reset requests per email address per time period
- The number of token validation attempts per token (to prevent concurrent brute force)
- The total number of reset requests from a source IP
Testing Password Reset Flows
A systematic assessment of a password reset implementation covers the following:
Token entropy. Request multiple resets in rapid succession and examine the tokens for patterns. Do they share a prefix? Do they appear to encode a timestamp? Are they shorter than 24 characters? Generate requests at known times and attempt to derive the token from the timestamp.
Token expiry. Generate a reset token and do not use it. Check whether it remains valid after 30 minutes, 60 minutes, and 24 hours.
Token invalidation. Complete a successful reset using a token, then attempt to use the same token a second time. The second attempt should fail.
Multiple active tokens. Request multiple resets in succession and attempt to use each generated token. Confirm that previous tokens are invalidated when a new one is issued.
Host header injection. Intercept the reset request and modify the Host header to a domain you control. Observe whether the resulting email contains a link pointing to the modified domain.
Response consistency. Submit the reset form with a known-registered email address and with an unregistered address. Compare response bodies, status codes, and response times.
Rate limiting. Submit the reset form in rapid succession from the same IP. Confirm that limits are enforced on both the submission endpoint and the token validation endpoint.
Token scope. Confirm that a token generated for one account cannot be used to reset a different account. Test by generating tokens for two accounts and attempting to submit each token against the other account's reset URL.
Why Account Recovery Deserves Primary-Path Security
Password reset is not a secondary authentication path. It is an authentication path that bypasses the primary credential entirely. An attacker who can exploit any step in the reset flow — predicting the token, injecting the host, brute-forcing a numeric code, or enumerating accounts — achieves the same outcome as stealing the password directly.
The frequency with which password reset weaknesses appear in production applications reflects how rarely this flow receives the same scrutiny as the login endpoint. Security reviews that cover brute force protection, session fixation, and credential hashing often miss the reset flow entirely.
For the account takeover techniques that build on password reset weaknesses, see the OAuth vulnerabilities knowledge article.
Need password reset and authentication flows tested against real-world attack patterns? Get in touch.