A password-reset endpoint. Generates a token, emails it. The token generator used `Math.random()` because it was the first thing the author reached for. Mesrai flagged it.
The vulnerable diff
// auth/reset-token.ts
export function generateResetToken(): string {
// BUG: Math.random is a non-cryptographic PRNG.
// V8 uses xorshift128+; state recovers from 5 outputs.
return Math.random().toString(36).slice(2);
}What is wrong
JavaScript's `Math.random` is implemented in V8 with the xorshift128+ algorithm — fast, well-distributed, completely non-cryptographic. Given a handful of consecutive outputs, the internal state of xorshift128+ can be recovered in under a second on a laptop, and from that state every past and future output is predictable. CWE-338 covers the misuse of weak PRNGs for security-sensitive token generation. The right primitive in Node is `crypto.randomBytes`, backed by the OS entropy pool.
The attack
Attack path: the application exposes any endpoint that returns a `Math.random` output to the user — a request id in a response header, a tracking pixel id, a UUIDv4 generated with `Math.random`. The attacker collects a few of those, recovers the state, then triggers a password reset on the victim's account and predicts the reset token before the email even arrives.
1. Attacker collects 5 consecutive Math.random outputs from any
public endpoint (request ids, UUIDv4, tracking ids).
2. Runs a published xorshift128+ state-recovery script
(~1 second on a laptop). Now knows the internal PRNG state.
3. Triggers password reset on victim's account. Predicts the
token. Resets the password before the victim even sees
the email.Public research has shipped working state-recovery code for V8 since 2014. This is not theoretical.
Mesrai's review comment
mesraipilot · Bot · reviewed 30 sec ago
[mesrai] [code-review] [Security] [CWE-338] [high]
`Math.random()` is V8's xorshift128+ PRNG. Not cryptographic. From any
sequence of ~5 consecutive outputs an attacker can recover the
internal state and predict every future output.
For a security-sensitive token use `crypto.randomBytes`:
import crypto from "crypto";
const token = crypto.randomBytes(32).toString("base64url");
The same rule applies to:
- session ids
- password reset tokens
- CSRF tokens
- API keys
- shareable invite links
- magic login tokens
Never `Math.random`, never the `uuid` v4 from packages backed by
`Math.random`. Use `crypto.randomUUID()` (Node 14.17+) which is backed
by the OS RNG.
Reference: OWASP Cheat Sheet on Randomness, CWE-338The fix
// auth/reset-token.ts — fixed
import crypto from "crypto";
export function generateResetToken(): string {
return crypto.randomBytes(32).toString("base64url");
}32 bytes from the OS entropy pool, base64url-encoded for safe URL transport. Unpredictable, sufficient entropy for any security purpose, one line. For UUID-shaped IDs the equivalent is `crypto.randomUUID()` — Node's built-in cryptographically-secure UUIDv4 generator since 14.17.
Why human review missed it
`Math.random` reads as 'a random number' to most engineers — the cryptographic-vs-statistical distinction is rarely part of day-to-day JS. The bug is also invisible at runtime: the tokens look random in logs, never collide in normal use. The vulnerability lives in the predictability under adversarial conditions. Mesrai's rule pack catches `Math.random` used near any of the canonical security-sensitive variable names.
Related rules + further reading
Mesrai rule pack: security/no-math-random-for-tokens — flags `Math.random` in token/session/csrf/reset variable contexts.
OWASP Cheat Sheet: Insecure Randomness.
CWE-338 — Use of Cryptographically Weak Pseudo-Random Number Generator.
Takeaway
Every security-sensitive token must come from `crypto.randomBytes` or `crypto.randomUUID`. `Math.random` is for animations, not auth.