Session tokens, password reset links, and email verification codes generated with Math.random() or Date.now() are predictable. CWE-338 (Use of Cryptographically Weak PRNG) and CWE-330 (Use of Insufficiently Random Values) describe how an attacker who knows the approximate generation time can enumerate the token space in seconds, enabling account takeover without ever knowing the victim's password. OWASP A02 (Cryptographic Failures) and NIST 800-53 SC-8 both require cryptographically secure randomness for security tokens. Six-digit numeric email codes have only one million possibilities — brute-forceable in under a minute against a rate-unlimited endpoint.
Critical because a predictable password-reset or session token lets an attacker take over any account without needing the user's credentials, bypassing authentication entirely.
Use crypto.randomBytes or crypto.randomUUID() for every security-sensitive token. Store reset tokens hashed (SHA-256), send the plaintext to the user:
import crypto from 'crypto'
const rawToken = crypto.randomBytes(32).toString('hex') // 256 bits
const storedToken = crypto.createHash('sha256').update(rawToken).digest('hex')
await db.passwordReset.create({
data: { token: storedToken, userId, expiresAt: new Date(Date.now() + 3_600_000) },
})
// Email rawToken to the user, never storedToken
UUID v4 (crypto.randomUUID()) is acceptable for session identifiers — 122 bits of randomness.
ID: security-hardening.auth-session.session-token-entropy
Severity: critical
What to look for: Count all session token generation points in the codebase. For each, examine how session tokens, auth tokens, password reset tokens, and email verification tokens are generated. Look for crypto.randomBytes, crypto.randomUUID, uuid(), nanoid(), or the equivalent secure random generators. Flag uses of Math.random(), Date.now(), sequential IDs, or predictable patterns.
Pass criteria: All security-sensitive tokens (sessions, CSRF, password reset, email verification) are generated using a cryptographically secure random number generator and are at least 16 bytes (128 bits) of entropy. UUIDs v4 qualify (122 bits of randomness). Report: "X token generation points found, all Y use cryptographically secure randomness."
Fail criteria: Any security token uses Math.random(), Date.now(), a sequential database ID, or any other non-cryptographic source of randomness. Tokens shorter than 128 bits of entropy also fail.
Skip (N/A) when: The project uses a managed auth library (NextAuth.js, Clerk, Auth0) that handles all token generation internally.
Detail on fail: Name the specific pattern. Example: "Password reset tokens generated with Math.random().toString(36) — predictable and guessable" or "Email verification tokens are 6-digit numeric codes — only 1M possibilities, brute-forceable"
Remediation: Use crypto.randomBytes (Node.js) or crypto.getRandomValues (Web Crypto API) for all security tokens:
import crypto from 'crypto'
// Generate a 32-byte (256-bit) hex token
const resetToken = crypto.randomBytes(32).toString('hex')
// Or use UUID v4 (128-bit, 122 bits of randomness)
const sessionToken = crypto.randomUUID()
// Store the token (hashed) and associate with user
await db.passwordReset.create({
data: {
token: crypto.createHash('sha256').update(resetToken).digest('hex'),
userId: user.id,
expiresAt: new Date(Date.now() + 3600 * 1000), // 1 hour
},
})
// Send the unhashed token to the user
await sendEmail(user.email, { resetToken })