Why You Should Never Use Math.random() for API Keys
Every JavaScript developer has written Math.random().toString(36).slice(2) at some point. It looks random, it produces different output each time, and it is easy to use. But for API keys, tokens, secrets, or anything security-related, Math.random() is fundamentally broken. Using it is not just bad practice — it is a vulnerability that attackers can and do exploit.
How Math.random() Actually Works
To understand the problem, you need to know what happens inside Math.random(). In V8 (Chrome, Node.js, Edge), it uses the xorshift128+ algorithm. This is a pseudorandom number generator (PRNG) that maintains 128 bits of internal state and produces a new output by applying XOR and bit-shift operations to that state.
// Simplified xorshift128+ (what V8 actually runs)
let state0 = seed0; // 64-bit internal state
let state1 = seed1; // 64-bit internal state
function xorshift128plus() {
let s1 = state0;
let s0 = state1;
state0 = s0;
s1 ^= s1 << 23;
s1 ^= s1 >> 17;
s1 ^= s0;
s1 ^= s0 >> 26;
state1 = s1;
return state0 + state1;
}
The critical point: this is a deterministic function. Given the same internal state, it always produces the same sequence. And since the state is only 128 bits, an attacker who can observe enough outputs can reconstruct the state and predict every future (and past) output.
The Predictability Problem
In 2015, researchers demonstrated that observing just a few outputs from V8's Math.random() is sufficient to recover the full internal state using a Z3 SMT solver. The tool, called v8_rand_buster, can predict the next output of Math.random() after observing as few as 3-5 sequential outputs.
Here is what this means in practice:
// An attacker observes these values from your API
// (e.g., by creating accounts and reading their API keys)
const observed = [
0.7483920165482937,
0.2917364820194738,
0.8362719405826371
];
// Using z3_solver or similar tool, they recover the PRNG state
// and predict the NEXT key your system will generate
const predicted_next = 0.4719283650172839;
// This is the EXACT value Math.random() will return next
This is not theoretical. If your API key generation endpoint is accessible and the keys are visible (even partially), an attacker can predict future keys assigned to other users.
The Entropy Problem
Even without state recovery attacks, Math.random() has a fundamental entropy problem. The PRNG state is only 128 bits, and it is seeded from a limited entropy source at engine startup. The actual output entropy per call is approximately 52 bits (the mantissa of a double-precision float).
| Source | Entropy per call | Predictable? | CSPRNG? |
|---|---|---|---|
Math.random() | ~52 bits | Yes, with 3-5 samples | No |
crypto.getRandomValues() | 8 bits per byte | No | Yes |
crypto.randomBytes() (Node) | 8 bits per byte | No | Yes |
A 32-character API key generated from Math.random() appears to have ~165 bits of entropy based on its character set, but the actual entropy is at most 128 bits (the PRNG state space) and practically much less once an attacker has observed any outputs.
Real Attack Scenarios
Scenario 1: Password Reset Token Prediction
A web application generates password reset tokens using Math.random(). An attacker creates several accounts, requests password resets, and observes the tokens in the reset URLs. Using these observations, they recover the PRNG state and predict the next token. They then request a password reset for the victim's account and use the predicted token to hijack it.
Scenario 2: API Key Prediction
An API platform generates keys using Math.random(). An attacker signs up for multiple free accounts and collects their API keys. After recovering the PRNG state, they predict keys assigned to paying customers and use those keys to access protected resources, exfiltrate data, or consume rate limits.
Scenario 3: Session Hijacking
A web framework generates session IDs using Math.random(). An attacker who knows their own session ID (from their browser cookies) can predict session IDs of other active users on the same server instance, enabling session hijacking without any network-level attack.
Why It Looks Random But Is Not
The output of Math.random() passes basic statistical randomness tests (frequency, runs, serial correlation). This is why developers trust it. But statistical randomness and cryptographic randomness are fundamentally different properties:
- Statistical randomness means the output has a uniform distribution and passes tests like Diehard or TestU01.
Math.random()achieves this. - Cryptographic randomness means the output is unpredictable — knowing any number of past outputs gives zero information about future outputs.
Math.random()fails this completely.
A PRNG can be statistically perfect and cryptographically worthless. xorshift128+ is exactly that.
The Correct Alternative: crypto.getRandomValues()
crypto.getRandomValues() is the Web Crypto API method that provides cryptographically secure pseudorandom numbers. It draws entropy from the operating system's entropy pool and is designed to be unpredictable.
// WRONG: Predictable, insecure
function badApiKey() {
return Math.random().toString(36).slice(2) +
Math.random().toString(36).slice(2);
}
// RIGHT: Cryptographically secure
function goodApiKey(byteLength = 32) {
const bytes = new Uint8Array(byteLength);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
The performance difference is negligible. On modern hardware, crypto.getRandomValues() generates 32 bytes in under 1 microsecond. There is no reason to use Math.random() for any security-sensitive operation.
Common Objections (and Why They're Wrong)
"It's just for internal use"
Internal APIs get compromised too. An employee with access to one key can predict others. Internal does not mean safe.
"We hash the keys before storing them"
Hashing protects stored keys from database breaches. It does nothing against prediction attacks. If an attacker can predict the next key before it is hashed, they can use it immediately.
"We add a timestamp to make it unique"
Timestamps are public information. Adding a predictable component to a predictable PRNG output does not create security. The attacker knows the timestamp too.
"Math.random() is fast enough for our scale"
This is not about speed. crypto.getRandomValues() is equally fast. The problem is that Math.random() is predictable, regardless of how fast it runs.
"No one would bother attacking our small API"
Automated scanning tools do not care how large your API is. Bots scan for weak key generation patterns across the entire internet. Your API key format is not a deterrent.
How to Audit Your Codebase
Search your codebase for dangerous patterns:
# Find all uses of Math.random() in JavaScript/TypeScript files
grep -rn "Math\.random" --include="*.js" --include="*.ts" src/
# Common dangerous patterns to look for:
# Math.random().toString(36)
# Math.random() * charset.length
# Math.floor(Math.random() * 256)
# 'xxxxxxxx'.replace(/x/g, () => Math.random()...)
Every instance of Math.random() used for key generation, token creation, session IDs, nonces, or any security-sensitive purpose must be replaced with crypto.getRandomValues().
Migration Example
Here is a before-and-after for a common pattern:
// BEFORE: Insecure
function generateToken() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
}
);
}
// AFTER: Secure
function generateToken() {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
const hex = Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return [
hex.slice(0, 8), hex.slice(8, 12), hex.slice(12, 16),
hex.slice(16, 20), hex.slice(20)
].join('-');
}
Or simply use the built-in crypto.randomUUID() available in all modern browsers and Node.js 19+:
const token = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Summary
Math.random()uses xorshift128+, which is deterministic and predictable after observing 3-5 outputs.- The effective entropy is at most 128 bits (PRNG state), and practically much less under attack.
- Real attacks exist: token prediction, API key prediction, session hijacking.
crypto.getRandomValues()is equally fast, universally supported, and cryptographically secure.- There is no valid reason to use
Math.random()for any security-sensitive operation.
Generate cryptographically secure API keys instantly with our free key generator, which uses crypto.getRandomValues() exclusively.
Further Reading
For a deep understanding of why Math.random() fails and how CSPRNGs actually work, read Real-World Cryptography. It covers entropy, PRNGs vs CSPRNGs, and the cryptographic primitives that make crypto.getRandomValues() secure.
To understand how attackers exploit weak randomness in web apps, The Web Application Hacker's Handbook is essential reading. For JavaScript-specific implementation details, JavaScript: The Definitive Guide covers the Web Crypto API in depth.