The top Stack Overflow answer for "generate a unique token in JavaScript" returns this:
Math.random().toString(36).substring(2)
It appears in password reset tokens, session IDs, invite codes, and API keys across the JavaScript ecosystem. I found it verbatim in calcom/cal.diy (~44K stars) — the MIT open-source edition of Cal.com:
const apiKey = `cal_live_${Math.random().toString(36).substring(2)}`;
Math.random() is a PRNG, not a CSPRNG. An attacker who observes consecutive outputs from a server-side process can recover the internal state and predict every future token. Here is the attack, the ESLint rule that catches this pattern automatically, and the one-line fix.
Why Math.random() is dangerous for tokens
Math.random() is a pseudo-random number generator — fast, but deterministic. Given V8's initial seed, its entire output sequence is fixed. An attacker who observes enough outputs can recover the 128-bit internal state and predict every future call.
// What you write:
const token = Math.random().toString(36).substring(2);
// V8 uses xorshift128+ under the hood.
// After observing a handful of consecutive Math.random() calls from the same process:
// → internal state recoverable (see d0nutptr/v8_rand_buster)
// → all future Math.random() outputs predictable
// → all future tokens predictable
CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator.
The exploit
xorshift128+ is algebraically invertible. Given full-precision doubles (52 bits each), the 128-bit internal state can be recovered. The v8_rand_buster implementation recovers state from 3–4 consecutive Math.random() outputs. The following shows the attack flow — the concrete values are illustrative; v8_rand_buster handles the actual algebra.
// Illustrative pseudocode — attacker collects consecutive Math.random() outputs:
// cal_live_k7f2m9p8x3z → Math.random() returned 0.38914728471823...
// cal_live_hq5r1n6wzt → Math.random() returned 0.72342901847612...
// cal_live_p9x4b2m7vy → Math.random() returned 0.15678234019876...
// Feed full-precision doubles to state-recovery:
// → internal xorshift128+ state reconstructed (s0, s1 recovered algebraically)
// Predict the next Math.random() call:
// → 0.59123847561029... → "cal_live_s2v8n3k1xq"
// Attacker knows the next key before it is issued.
xorshift128+ is algebraically invertible: given output values (full 52-bit precision), the 128-bit internal state can be solved. v8_rand_buster recovers state from 3–4 consecutive outputs. The practical requirement is consecutive outputs from the same process — which is achievable when key generation is observable and isolated.
Two attack surfaces, depending on where the code runs:
Client-side (the calcom/cal.diy case): The key is generated in the user's own browser. Client-side key generation exposes the credential in the client's environment before it reaches the server — visible in network logs, browser devtools, extensions, or XSS. The PRNG weakness is secondary to the architectural problem of minting a secret in the browser at all.
Server-side (the more general case): If Math.random() generates tokens in a server process, an attacker who observes multiple consecutive tokens (e.g., via multiple signups, password resets, or API requests) can recover internal state and predict the next token — including tokens for other users. This is the scenario where state recovery attacks apply most cleanly.
Where this pattern appears
// Top Stack Overflow pattern for "generate unique token":
const token = Math.random().toString(36).substring(2, 15);
// Session ID (with Date.now() — adds a predictable timestamp, not additional unpredictability):
const sessionId = Date.now().toString(36) + Math.random().toString(36).substring(2);
// Invite code:
const inviteToken = Math.random().toString(36).substring(2, 8).toUpperCase();
// The calcom/cal.diy pattern:
const apiKey = `cal_live_${Math.random().toString(36).substring(2)}`;
The first three are less exploitable than the calcom/cal.diy pattern — partial substrings leak less state per observation. But all four use a PRNG for a value that should be a CSPRNG.
Why it survives code review
-
Math.random()literally has "random" in the name - The output looks unpredictable:
k7f2m9p8x3z - No runtime errors — tokens generate correctly, tests pass
- Reviewers focus on the business logic, not PRNG security properties
Nobody reviews token generation and asks "is this the right kind of random?" The ESLint rule asks it for you.
Source context: calcom/cal.diy is the MIT open-source edition of Cal.com (the enterprise codebase runs separately under AGPL as
calcom/cal.com). TheMath.random()line is in a client-side React component. For client-side code, the architectural problem — generating a secret in the browser — is the primary concern. The PRNG weakness is secondary, but it compounds. The state-recovery attack in the Exploit section applies directly to server-side equivalents of this pattern.
The ESLint rule that catches it
eslint-plugin-node-security/no-math-random-crypto fires when Math.random() is assigned to a variable whose name matches any of 18+ security-sensitive patterns: token, key, secret, session, auth, csrf, nonce, otp, code, verify, and more.
On false positives: React's list reconciliation key prop doesn't trigger this — the rule checks Math.random() assignments, not JSX attributes. code for HTTP status codes or country codes won't trigger either, because the rule only fires when Math.random() is the value being assigned. If you have legitimate non-security uses of a key variable fed by Math.random() (rare but possible), configure allowInTests: true or use an ESLint disable comment with a note explaining why.
node-security/no-math-random-crypto
CWE-338 | Math.random() used in cryptographic context 'apiKey'
Use crypto.randomBytes() or crypto.randomUUID() instead
apps/web/components/apps/make/Setup.tsx (line varies by version)
This fires on the calcom/cal.diy line.
The fix — server-side vs client-side
Server-side (Node.js API route, Express, NestJS):
import crypto from 'node:crypto';
// Opaque token — hex string, 48 characters:
const apiKey = `cal_live_${crypto.randomBytes(24).toString('hex')}`;
// UUID format:
const id = crypto.randomUUID();
Client-side / browser (the deeper issue in the calcom/cal.diy case):
Generating secret API keys client-side is itself the architectural problem — the key is visible in client memory and potentially in browser devtools. The right fix is to move key generation server-side and return the key via an authenticated API call. If you must generate randomness in the browser, use Web Crypto:
// Web Crypto — available in all modern browsers and Node.js 19+:
const array = new Uint8Array(24);
globalThis.crypto.getRandomValues(array);
const token = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
globalThis.crypto.getRandomValues() uses the OS CSPRNG and is safe in both browser and Node.js environments.
The config
// eslint.config.mjs
import nodeSecurity from 'eslint-plugin-node-security';
import tsParser from '@typescript-eslint/parser';
export default [
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
languageOptions: { parser: tsParser },
plugins: { 'node-security': nodeSecurity },
rules: {
'node-security/no-math-random-crypto': 'error',
},
},
];
npm install --save-dev eslint-plugin-node-security
npx eslint src/
Full rule documentation at eslint.interlace.tools.
Note: this rule catches the PRNG problem. It won't flag client-side key generation as an architectural issue — that's a separate concern for code review.
If you're auditing older code for this class of vulnerability more broadly, the 30-minute security audit protocol covers Math.random() alongside credential handling, JWT configuration, and input validation in a single pass.
Have you run this against your codebase yet? I'm specifically curious where it shows up — expected places like token generation, or somewhere that surprised you.
Part of the Exploit Analysis series. See also:
Exploit Analysis: The JWT Algorithm 'none' Attack (And the Guard)
📦 eslint-plugin-node-security · Rule docs
GitHub | X | LinkedIn | Dev.to | ofriperetz.dev











