I found this code in calcom/cal.diy (~44K GitHub stars), the open-source scheduling platform — apps/web/components/apps/make/Setup.tsx, line 38:
const apiKey = `cal_live_${Math.random().toString(36).substring(2)}`;
Given a handful of consecutive cal_live_ keys generated by the same process, an attacker can predict the next one. Not guess — predict.
Here is why, and the ESLint rule that catches this pattern in 3 seconds.
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
The following is illustrative — real state recovery requires the full 52-bit mantissa of each double, not the truncated values shown here. The v8_rand_buster tool handles the actual implementation.
// 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.
Practical scope: exploitation requires consecutive outputs from the same V8 process. Process restarts reseed. Interleaved Math.random() calls from React rendering or library internals between key generations make recovery harder — though not impossible when the generation is isolated and observable. The attack surface is architecture-dependent; the fundamental problem is not.
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.
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.
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:38:18
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.
Context: calcom/cal.diy is the community open-source fork of Cal.com (released under MIT after Cal.com went commercial). The
Math.random()line is in a client-side Make integration helper (apps/web/components/apps/make/Setup.tsx:38) — not the core authentication or credential issuance path of the commercial Cal.com product. The code is public, inmain, and was included in our benchmark corpus as a real-world example of CWE-338 in an active open-source codebase. Report patterns like this to the maintainers of any project you audit before publishing.
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
Top comments (0)