Math.random() is not cryptographically secure — its output is predictable if you know the seed. Learn about entropy, CSPRNGs, and how to generate secure random values in JavaScript.
Math.random() feels random. The numbers look unpredictable. But computers are deterministic machines — they cannot generate true randomness. Instead, they use pseudo-random number generators (PRNGs): algorithms that take a "seed" (initial state) and produce a sequence that looks random.
The problem: if an attacker can determine the seed or observe enough output, they can predict all future values. For Math.random(), this is easier than you think.
Most JavaScript engines implement Math.random() using xorshift128+ (V8, SpiderMonkey) or xoshiro256++ (JSC). These are fast PRNGs optimized for statistical quality — not security.
// V8 xorshift128+ implementation (simplified)
let state0 = /* seeded from system entropy at startup */
let state1 = /* seeded from system entropy at startup */
function mathRandom() {
let s1 = state0
let s0 = state1
state0 = s0
s1 ^= s1 << 23
s1 ^= s1 >> 17
s1 ^= s0
s1 ^= s0 >> 26
state1 = s1
return (state0 + state1) / 2**64
}
In 2016, researchers showed that observing just 3 consecutive Math.random() outputs is sufficient to recover the internal 128-bit state of V8's PRNG — allowing prediction of all future and past values.
// ❌ Dangerous: token generated with Math.random()
function generateToken() {
return Math.random().toString(36).slice(2) + Date.now().toString(36)
}
// An attacker who can observe 3 tokens from your server can predict all future tokens
// — including password reset links, session IDs, and CSRF tokens
The crypto.getRandomValues() API uses the operating system's CSPRNG (Cryptographically Secure PRNG) — on Linux this is /dev/urandom (BLAKE2-based entropy pool), on macOS/iOS it's the SecRandomCopyBytes API. These are designed specifically to be unpredictable.
// ✅ Cryptographically secure random bytes
const bytes = new Uint8Array(32)
crypto.getRandomValues(bytes)
// [45, 182, 23, 88, ...] — 32 unpredictable bytes
// ✅ Secure random integer in range [0, max)
function secureRandomInt(max) {
const limit = 2**32 - (2**32 % max) // Avoid modulo bias
let value
do {
const arr = new Uint32Array(1)
crypto.getRandomValues(arr)
value = arr[0]
} while (value >= limit)
return value % max
}
// ✅ Secure random token (hex)
function secureToken(byteLength = 32) {
const bytes = new Uint8Array(byteLength)
crypto.getRandomValues(bytes)
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("")
}
// "a3f8c21b9e047d6..." (64 hex chars = 32 bytes = 256 bits)
// ✅ Secure random token (base64url)
function secureTokenB64(byteLength = 32) {
const bytes = new Uint8Array(byteLength)
crypto.getRandomValues(bytes)
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
}
// ✅ Built-in secure UUID v4 generation (Node 19+, modern browsers)
crypto.randomUUID()
// "3b1b1a4c-5a9f-4d0e-8d7a-2c1f3e9b0a1d"
// This is equivalent to:
function manualUUID() {
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40 // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80 // variant
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)}`
}
Math.random() is perfectly appropriate for non-security use cases:
// ✅ Fine: random card shuffle in a game
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // OK here
[array[i], array[j]] = [array[j], array[i]]
}
return array
}
// ❌ Not fine: shuffling cryptographic keys, generating tokens, passwords
| Use Case | API |
|---|---|
| Password reset tokens | crypto.getRandomValues() |
| Session IDs | crypto.getRandomValues() |
| CSRF tokens | crypto.getRandomValues() |
| UUID generation | crypto.randomUUID() |
| Passphrases / random strings | crypto.getRandomValues() |
| Game randomness | Math.random() ✓ |
| UI animations | Math.random() ✓ |
All HeoLab security tools — Random String Generator, Passphrase Generator, Password Generator — use crypto.getRandomValues() exclusively.
Math.random() is a statistical tool, not a security tool. Its internal state is recoverable from a small number of observations, making any token, password, or ID generated from it potentially predictable. Use crypto.getRandomValues() for anything security-sensitive — it is equally fast, available everywhere, and genuinely unpredictable.
Random String Generator
Generate random strings with configurable length, count, and character set — hex, alphanumeric, base64url, and more.
UUID Generator
Generate UUIDs v4 and v7 in bulk. Supports multiple formats: standard, no-dashes, braces, and URN.
Passphrase Generator
Generate secure, memorable passphrases from a word list. Shows entropy bits and strength rating.
Password Generator
Generate cryptographically secure passwords with custom length, character sets, and strength analysis.