Insecure Password Reset Flow
debt(d5/e5/b3/t7)
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.
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.
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.
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.
Also Known As
TL;DR
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
Why It Matters
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
// 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);
// 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]);