Predictable Token
debt(d5/e3/b3/t7)
Closest to 'specialist tool catches it' (d5), semgrep/psalm/phpstan rules can flag rand()/mt_rand()/md5(uniqid()) patterns used as tokens, but it requires the security-focused ruleset, not default lint.
Closest to 'simple parameterised fix' (e3), the quick_fix is to swap to bin2hex(random_bytes(32)) at each token generation site — straightforward replacement but typically needs touching every generation point.
Closest to 'localised tax' (b3), token generation is usually centralized in a few helpers (password reset, CSRF, API keys); the bad choice doesn't gravitationally shape the rest of the codebase.
Closest to 'serious trap' (t7), the misconception that md5(time()) or md5(uniqid()) is unpredictable is widespread and contradicts intuition — hashing feels like it adds security but timestamp entropy is trivially brute-forceable.
Also Known As
TL;DR
Explanation
Security tokens (password reset links, session IDs, API keys) must be unpredictable. Using md5(time()) produces only 86,400 distinct values per day — an attacker can brute-force all of them in seconds. rand() and mt_rand() are pseudorandom and their output can be predicted from a small number of observed values. The only correct source for security-sensitive random data is a CSPRNG: bin2hex(random_bytes(32)) in PHP.
Common Misconception
Why It Matters
Common Mistakes
- Using rand() or mt_rand() which are seeded by time and guessable.
- Using MD5 or SHA1 of a timestamp — the timestamp is known or estimable.
- Sequential integer tokens for anything security-sensitive.
- Short tokens (less than 128 bits) that make brute force feasible even with secure randomness.
Code Examples
// Predictable — attacker can guess or brute-force
$token = md5($userId . time());
$token = uniqid(); // only 7 bytes of entropy
$token = rand(); // seeded from time — predictable
// 32 bytes = 256 bits of entropy — computationally unguessable
$token = bin2hex(random_bytes(32)); // 64 hex chars
// Store hash in DB, send raw token to user
$stored = hash('sha256', $token);
$db->insert('password_resets', [
'user_id' => $userId,
'token_hash' => $stored,
'expires_at' => date('Y-m-d H:i:s', time() + 3600),
]);
// Verify with constant-time comparison:
if (!hash_equals($stored, hash('sha256', $inputToken))) abort(400);
if ($record->expires_at < now()) abort(400);