Insecure Password Reset Flow
Also Known As
password reset vulnerability
reset token weakness
account recovery flaw
TL;DR
Weaknesses in the password-reset mechanism allow attackers to take over accounts without knowing the original password.
Explanation
Common flaws include: predictable reset tokens (timestamp-based or sequential), tokens that don't expire, tokens that remain valid after use, user enumeration via reset responses, and Host header injection into reset emails. A secure reset flow generates a cryptographically random token with random_bytes(32), stores a hash of it in the database, expires it after 15–60 minutes, invalidates it immediately on use, and uses a constant-time comparison (hash_equals) when verifying.
Common Misconception
✗ Password reset flows are low risk because they only affect forgotten passwords. Reset flaws are a primary account takeover vector — predictable tokens, tokens that never expire, and host-header injection in reset emails all lead to full account compromise.
Why It Matters
A weak password reset flow is often the easiest path to account takeover — it bypasses authentication entirely if tokens are predictable or reusable.
Common Mistakes
- Using rand() or uniqid() to generate reset tokens — both are predictable.
- Not expiring reset tokens after a short window (e.g. 15-30 minutes).
- Not invalidating a reset token after first use — allows repeated use of a stolen token.
- Sending the new password in the email instead of a single-use reset link.
Code Examples
✗ Vulnerable
// Predictable token, no expiry, no single-use enforcement
\$token = md5(\$user->email . time());
\$user = DB::where('reset_token', \$_GET['token'])->first();
if (\$user) allowPasswordReset(\$user);
✓ Fixed
// 1. Cryptographically secure token
\$token = bin2hex(random_bytes(32));
\$hash = hash('sha256', \$token); // store hash, send raw
\$expiry = now()->addHour();
DB::table('password_resets')->insert([
'user_id' => \$user->id,
'token_hash' => \$hash,
'expires_at' => \$expiry,
'used' => false,
]);
// 2. Verify — constant-time, expiry check, single-use
\$record = DB::table('password_resets')
->where('token_hash', hash('sha256', \$inputToken))
->where('expires_at', '>', now())
->where('used', false)
->first();
if (!\$record) abort(400);
DB::table('password_resets')->where('id', \$record->id)->update(['used' => true]);
References
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
15 Mar 2026
Edited
22 Mar 2026
Views
30
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 0
No pings yet today
No pings yesterday
Amazonbot 8
Perplexity 5
ChatGPT 5
Google 3
Ahrefs 1
Also referenced
How they use it
crawler 22
Related categories
⚡
DEV INTEL
Tools & Severity
🔴 Critical
⚙ Fix effort: Medium
⚡ Quick Fix
Generate reset tokens with random_bytes(32), store only the hash, expire after 15 minutes, invalidate immediately on use, and use hash_equals() to compare
📦 Applies To
PHP 5.0+
web
🔗 Prerequisites
🔍 Detection Hints
Password reset token generated with md5(time()) uniqid() mt_rand() or stored unhashed in DB
Auto-detectable:
✓ Yes
semgrep
phpstan
⚠ Related Problems
🤖 AI Agent
Confidence: High
False Positives: Medium
✗ Manual fix
Fix: Medium
Context: File
Tests: Update
CWE-640
CWE-287