← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Insecure Password Reset Flow

security CWE-640 OWASP A7:2021 CVSS 8.1 PHP 5.0+ Intermediate

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]);

Added 15 Mar 2026
Edited 22 Mar 2026
Views 30
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 1 ping T 0 pings F 0 pings S 2 pings S 1 ping M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 2 pings T 0 pings F 0 pings S 2 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Amazonbot 8 Perplexity 5 ChatGPT 5 Google 3 Ahrefs 1
crawler 22
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

✓ schema.org compliant