June 5, 2026
Password Reset Flow Testing — The Most Overlooked Account Takeover Vulnerability
Hackers don’t always crack passwords. Sometimes they just click “Forgot Password?” and walk right in through a broken back door.
Yamini Yadav_369
6 min read
The password reset feature exists on almost every website. It looks simple. But underneath, it hides some of the most dangerous and most ignored security bugs in web development. In this blog, we'll break down every vulnerability, explain it in plain English, show real examples, and tell you exactly how to fix each one.
No jargon. No assumed knowledge. Anyone can learn this.
What Is a Password Reset Flow?
When you forget your password, most websites offer a "Forgot Password?" link. You enter your email, they send you a link or code, and you create a new password. That entire process is called the password reset flow.
Think of it like this:
You lost your house key. You call the landlord for a spare. If the landlord doesn't verify who you are before handing it over — a stranger could pretend to be you and get access to your home.
Password reset bugs work the same way. Poor verification lets attackers "get the key" to your account without ever knowing your actual password.
Why Attackers Love This Feature
Cracking a password directly is hard. Passwords are hashed and encrypted. But the reset flow is a side door — a second way into the account. And that door is often left wide open.
Here's why attackers target it:
- There's often no limit on how many reset attempts can be made
- Reset tokens are sometimes short and guessable
- Links are often valid forever
- Tokens can leak through browser history, server logs, or email headers
The 8 Most Common Vulnerabilities
Vulnerability 1 — Token Never Expires
What it means: The reset link works forever — even if it was sent months or years ago.
Real example: An attacker searches a victim's old emails (maybe they have temporary access to the inbox). They find a password reset email from two years ago. The link still works. They click it, set a new password, and take the account.
How to fix it: Set a short expiry — 15 to 30 minutes is the standard. After that, delete the token from the database. If the user didn't use it in time, they can simply request a new one.
Vulnerability 2 — Weak or Short Token
What it means: The reset code is something short like 482910 (6 digits) or something predictable like the user's ID number. Short tokens can be guessed automatically by a script.
Real example: A website sends a 6-digit OTP for password reset. There are only 1,000,000 possible combinations (000000 to 999999). An attacker writes a simple script that tries every combination one by one. With no limit on attempts, the script finishes in minutes and finds the correct code.
for token in range(000000, 999999):
try /reset?token={token}
if "Enter new password" in response:
print("Found it! Account is mine.")for token in range(000000, 999999):
try /reset?token={token}
if "Enter new password" in response:
print("Found it! Account is mine.")How to fix it: Use a cryptographically random token of at least 32 bytes. In Python: secrets.token_urlsafe(32). In Node.js: The result is a 64-character string that is practically impossible to guess.
Vulnerability 3 — Token Can Be Used Multiple Times
What it means: After using a reset link to change the password, the same link still works. It was never marked as "used."
Real example: Alice resets her password using a link. An attacker who had intercepted that same email link tried it 10 minutes later—and it still worked. The attacker resets Alice's password again and takes over the account.
How to fix it: As soon as a token is used successfully, delete it or mark it as used in the database. Any future request with that token should return an error saying the link is invalid or already used.
Vulnerability 4 — No Rate Limiting
What it means: The server allows unlimited reset requests or token guesses with no slowdown, no CAPTCHA, and no block.
Real example: An attacker writes an automated script that fires 10,000 reset requests per minute. No alert is triggered. No IP block happens. The brute force succeeds.
How to fix it:
- Max 3–5 reset requests per email per hour
- Max 5 token verification attempts before the token is invalidated
- Add CAPTCHA for suspicious behavior.
- Consider temporarily locking the reset feature after repeated failed attempts
Vulnerability 5 — Host Header Injection
What it means: The server builds the reset link using the Host header from the incoming HTTP request. An attacker can change that header to point to their own server. The victim receives an email with a link to the attacker's site. When the victim clicks it, the attacker captures the token.
Real example:
Normal request:
POST /forgot-password
Host: bank.com
email: victim@gmail.comPOST /forgot-password
Host: bank.com
email: victim@gmail.comEmail sent to victim: https://bank.com/reset?token=abc123 ✅
Attacker's request:
POST /forgot-password
Host: attacker.com
email: victim@gmail.comPOST /forgot-password
Host: attacker.com
email: victim@gmail.comEmail sent to victim: https://attacker.com/reset?token=abc123
Victim clicks the link → attacker's server logs the token → attacker uses it on the real bank site → account taken.
How to fix it: Never use $_SERVER['HTTP_HOST'] or request.headers['host'] to build URLs. Always use a hardcoded config value:
APP_URL=https://bank.comAPP_URL=https://bank.comBuild every reset link from this config value only.
Vulnerability 6 — Token Exposed in URL and Logs
What it means: When the token is part of the URL (a GET parameter), it gets saved in:
- Browser history
- Server access logs
- Proxy and CDN logs
- Third-party analytics via the Referer header (Google Analytics, Facebook Pixel, etc.)
Real example: A company's IT admin has access to the web server logs. They notice entries like:
GET /reset-password?token=SuperSecretToken123GET /reset-password?token=SuperSecretToken123They copy that token, visit the URL, and reset a colleague's password. No hacking was required — it was sitting in a plain text log file.
How to fix it:
- After validating the token, switch to a POST-based session flow so the token leaves the URL
- Add the header
Referrer-Policy: no-referrerto the reset page - Store the token in a server-side session instead of keeping it in the URL
Vulnerability 7 — Old Sessions Not Invalidated
What it means: When a user resets their password, all their previously active login sessions remain alive. If an attacker had stolen a session cookie, they still have access even after the password change.
Real example: Bob notices suspicious activity and resets his password. But the attacker had already stolen Bob's session cookie. Since old sessions were never killed, the attacker's browser session still works — even with the new password. The attacker remains logged in.
How to fix it: After a successful password reset:
- Invalidate every active session for that account
- Issue a brand new session only for the current request
- Send the user a notification email: "Your password was changed. If this wasn't you, contact support."
Vulnerability 8 — Username Enumeration via Error Messages
What it means: The app reveals whether an email address is registered by showing different responses — like "Email not found" vs. "Reset link sent." Attackers use this to build a list of valid accounts to target.
Real example: An attacker tests 10,000 email addresses against the forgot-password form. For most, they get "Email not found." For some, they get "A reset link has been sent." They now have a confirmed list of real users—perfect targets for phishing or brute-force attacks.
How to fix it: Always show the same message, no matter what:
"If this email address is registered, you will receive a password reset link shortly."
Never confirm or deny whether the email exists.
What a Safe Password Reset Flow Looks Like
Here's the complete, secure process step by step:
Step 1—User enters their email. Show the same message regardless of whether the email exists. Never leak account information.
Step 2—The server generates a strong token. Use crypto.randomBytes(32) → 64-character random hex string. Store a SHA-256 hash of the token in the database — not the token itself. Set expiry to 15–30 minutes.
Step 3—Send the email using a hardcoded domain. Build the reset link from your app config: https://yoursite.com/reset?token=.... Never from the request headers.
Step 4—Verify the token for use. Check that the token's hash matches the database record, that it has not expired, and that it has not been used before. Enforce rate limits.
Step 5—Reset password, destroy token, kill sessions. After a successful reset, delete the token, invalidate all existing sessions, hash and save the new password, and send the user a confirmation email.
Things to avoid (bad practices):
- Using a short numeric OTP (4–6 digits) without rate limiting
- Building the reset URL from
$_SERVER['HTTP_HOST']orreq.headers.host - Storing the plaintext token in the database
- Token valid for 24+ hours or forever
- Allowing the same token to be used more than once
- Not invalidating old sessions after password change
- Showing "Email not found" when a wrong email is entered
- Passing the token as a GET parameter in the URL
Things to do (good practices):
- Use
crypto.randomBytes(32)"or"secrets.token_urlsafe(32)for tokens - Store only the SHA-256 hash of the token in the database
- Set token expiry to 15–30 minutes
- Invalidate the token immediately after first use
- Rate limit: max 3–5 reset requests per email per hour
- Hardcode your domain in config and always use it for reset links
- Terminate all active sessions after password reset
- Add
Referrer-Policy: no-referrerheader to the reset page - Send the user an email notification after every password change
- Always show the same response message whether the email exists or not
The password reset feature is a front door that many developers forget to lock properly. It's not glamorous. It doesn't involve complex algorithms. But it is one of the most commonly exploited paths to account takeover — because it is simple, trusted, and overlooked.
Test every reset flow like an attacker would. Because attackers definitely will.
Found this helpful? Share it with your team. The best security is the kind everyone understands.