I found this in our benchmark corpus, extracted verbatim from . Once the state is known, every future Math.random() call is predictable — including every future token it generates.
// What you write:
const apiKey = `cal_live_${Math.random().toString(36).substring(2)}`;
// ↑ non-cryptographic, state-recoverable
// After observing ~20 outputs, an attacker runs state recovery:
// → internal 128-bit state reconstructed
// → every future Math.random() call predicted
// → every future API key predicted
CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator.
Why it survives code review
The pattern looks reasonable at a glance:
Math.random()literally has "random" in the name- The output looks like a long, unpredictable string:
k7f2m9p8x3z
- It works — tokens generate correctly, no runtime errors, tests pass
- Reviewers focus on business logic, not PRNG security properties
Nobody reviews token generation and asks "is this the cryptographically correct kind of random?" That is a security-specific question that most engineers do not carry into review. The ESLint rule asks it for you.
Where it appears
The Cal.com example was extracted from their Make integration setup at the time of our benchmark snapshot — it may have been updated since. The pattern itself is common:
// Top Stack Overflow pattern for "generate unique token":
const token = Math.random().toString(36).substring(2, 15);
// Quick session ID:
const sessionId = Date.now().toString(36) + Math.random().toString(36).substring(2);
// Note: concatenating Date.now() adds a predictable timestamp — it adds length
// but no additional unpredictability. The Math.random() entropy is unchanged.
// Invite code:
const inviteCode = Math.random().toString(36).substring(2, 8).toUpperCase();
// The Cal.com pattern:
const apiKey = `cal_live_${Math.random().toString(36).substring(2)}`;
All four are caught by the same ESLint rule. All four are vulnerable to state recovery.
The ESLint rule that catches it
eslint-plugin-node-security/no-math-random-crypto fires whenever Math.random() is assigned to a variable whose name suggests a security-sensitive context: token, key, secret, session, csrf, nonce, auth, otp, and .
Have you scanned your codebase for this pattern yet? Post what you find — I am specifically curious whether it shows up in places you expected or somewhere surprising.
Part of the
📦
| |
SOCIAL SHARE CARD GENERATOR