← Home ← Codex ← DEBT ← Engine
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
debt(d5/e5/b3/t7)
d5 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'specialist tool catches it' (d5). The detection_hints list semgrep and phpstan as tools, and the code_pattern targets specific insecure token generation functions (md5(time()), uniqid(), mt_rand()). These are specialist SAST tools that catch common cases but won't catch every variant (e.g. missing expiry logic, host-header injection in reset emails), so it sits squarely at d5 rather than d3.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix requires generating tokens with random_bytes(32), storing only the hash, adding expiry timestamps, invalidating on use, and using hash_equals() for comparison. This is not a single-line swap — it touches token generation, storage schema (hashing), email dispatch logic, and the redemption/validation endpoint. Multiple files and coordinated changes are needed, placing this at e5.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). The applies_to context is web only, and the flaw is scoped to the password reset subsystem — a relatively contained part of the codebase. It doesn't impose a persistent tax across unrelated work streams, but the fix does require ongoing discipline (e.g. keeping tokens short-lived, always invalidating on use), so b3 is appropriate rather than b1.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field explicitly states that developers believe reset flows are low risk because they only affect forgotten passwords, when in reality they are a primary account takeover vector. Using rand(), uniqid(), or md5(time()) feels natural to many developers familiar with generating identifiers, and the failure mode (predictable tokens, no expiry, reuse) is non-obvious and contradicts the assumption that 'it's just a convenience feature.' This rates t7 rather than t5 because the misconception is categorical (low risk vs. full account takeover), not merely a subtle edge case.

About DEBT scoring →

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 194
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
2 pings T 0 pings F 2 pings S 0 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 1 ping T 0 pings F 1 ping S 2 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F
No pings yet today
No pings yesterday
Amazonbot 10 PetalBot 7 Ahrefs 6 Perplexity 5 ChatGPT 5 Google 5 Bing 5 SEMrush 5 Scrapy 5 Applebot 2 Claude 1 Meta AI 1 Twitter/X 1 Brave Search 1
crawler 57 crawler_json 2
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