Zum Hauptinhalt springen
MakeMyPasswords

Why Math.random() Isn't Random Enough for Security

·5 min read

Call Math.random() in your browser console right now. You'll get something like 0.7281940548498037 — looks random. Do it again, different number. Feels random. But if you used that number to generate a password, pick a session token, or seed a cryptographic key, you'd have a security vulnerability. Here's why.

What Math.random() Actually Does

Math.random() is not magic. It runs a deterministic algorithm — given the same internal state, it produces the same sequence of numbers every time. The specific algorithm depends on the JavaScript engine:

V8 (Chrome, Node.js, Edge) uses Xorshift128+. It maintains two 64-bit state variables (s0 and s1) and updates them each call with a combination of XOR and bit-shift operations:

result = s0 + s1
s1 = s1 XOR (s1 << 23)
s0, s1 = s1, s0 XOR s1 XOR (s0 >> 17) XOR (s1 >> 26)
return result / 2^64

That's it. No entropy from hardware, no thermal noise, no atmospheric randomness. Just two numbers being mutated through arithmetic. The output looks uniform if you plot it, and it passes basic statistical tests like TestU01's SmallCrush. But "passes basic statistical tests" is a very different bar from "secure."

SpiderMonkey (Firefox) also uses Xorshift128+. JavaScriptCore (Safari) uses a variant called GameRand. All three are fast PRNGs designed for simulation and games, not for security.

What "Predictable" Actually Means

When cryptographers say Math.random() is "predictable," they don't mean you can guess the next number by staring at the previous one. They mean something more precise: if an attacker observes enough outputs, they can reconstruct the internal state and predict every future (and past) output.

For Xorshift128+, the internal state is 128 bits. The output is derived directly from the state with no one-way function applied. In 2015, researchers demonstrated that observing just a few consecutive Math.random() outputs was sufficient to recover the full state using a technique called Z3 SMT solving. With the state recovered, every subsequent call to Math.random() is known to the attacker before your code even makes it.

This isn't theoretical. Here's a simplified attack scenario: a web application generates a password reset token using Math.random(). The same page also uses Math.random() for UI animations — maybe randomizing the position of floating background particles. An attacker loads the page, observes the particle positions (which are derived from Math.random() calls), solves for the internal state, and predicts the reset token that was generated server-side in the same V8 isolate.

PRNG vs. CSPRNG

The distinction here has a formal name:

A PRNG (pseudorandom number generator) produces output that is statistically uniform — it looks random if you run chi-square tests or plot histograms. Math.random() is a PRNG. So is the Mersenne Twister used by Python's random module, and the linear congruential generators in older C standard libraries.

A CSPRNG (cryptographically secure pseudorandom number generator) adds a critical property: unpredictability. Even if an attacker sees every output you've ever produced, they cannot predict the next one without breaking a computational assumption that's believed to be hard (like factoring large integers or inverting a hash function). CSPRNGs are seeded from real entropy — hardware interrupts, mouse movements, disk timing jitter — and they process that entropy through constructions like ChaCha20 or AES-CTR that destroy the relationship between internal state and output.

The gap between these two categories is not a matter of degree. A PRNG is fundamentally breakable by design (it's a simple function). A CSPRNG is secure by design (it would require breaking a mathematically hard problem).

How crypto.getRandomValues() Works

The Web Crypto API provides crypto.getRandomValues(), which fills a typed array with cryptographically secure random bytes. When you call it in a browser:

  1. The browser requests bytes from the operating system's CSPRNG
  2. On Linux, that's /dev/urandom (backed by ChaCha20 since kernel 4.8)
  3. On macOS, it's SecRandomCopyBytes (backed by Fortuna/Yarrow)
  4. On Windows, it's BCryptGenRandom (backed by AES-CTR-DRBG)

The OS CSPRNG continuously gathers entropy from hardware sources — interrupt timing, CPU jitter, RDRAND instructions on modern Intel/AMD chips — and mixes it into a state pool. The output passes through a cryptographic construction that makes state recovery computationally infeasible.

This means even if an attacker can observe every byte you've ever gotten from crypto.getRandomValues(), they cannot predict the next byte. That's the guarantee Math.random() simply cannot make.

Real Vulnerabilities From Weak Randomness

This isn't just a theoretical exercise. Weak PRNGs have caused real security failures:

Online gambling exploits (2000s–2010s): Multiple online poker and casino platforms used seeded PRNGs for card shuffling. In one well-documented case, researchers at Software Security Solutions reverse-engineered the shuffling algorithm of a major poker site in 1999. The PRNG was seeded with the system time in milliseconds — only about 86 million possible seeds per day. They could determine the seed from the first few dealt cards and know every player's hand for the entire game.

PHP session token prediction: Older versions of PHP generated session IDs using a combination of the remote IP, timestamp, and a linear congruential generator. Attackers could enumerate possible session IDs and hijack active sessions. This led to CVE-2011-3267 and prompted PHP to switch to OS-sourced randomness for session generation.

OAuth token prediction: Several OAuth implementations in the early 2010s generated authorization codes and access tokens using language-level PRNGs (Java's java.util.Random, Python's random module). Attackers who could obtain a few valid tokens could predict future tokens. This was widespread enough that the OAuth 2.0 spec (RFC 6749) explicitly requires that tokens be generated using a CSPRNG.

Minecraft seed cracking: While not a security vulnerability per se, Minecraft world seeds are derived from Java's java.util.Random (a 48-bit linear congruential generator). Players built tools that could determine a server's world seed by observing a handful of in-game features — slime chunk locations, structure positions, biome boundaries — effectively reconstructing the entire generator state from partial observations.

When to Use Which

The rule is simpler than the theory:

Use crypto.getRandomValues() (or a wrapper like our secureRandomInt()) for anything that could serve as a secret or that an attacker would benefit from predicting: passwords, tokens, session IDs, nonces, API keys, encryption keys, WiFi passwords, one-time codes.

Use Math.random() when unpredictability doesn't matter: dice rolls in a board game, shuffling a playlist, randomizing UI animations, picking a random color, choosing which tip to display. If someone "predicting" the next output gives them no advantage, Math.random() is fine — and it's faster.

The failure mode is always the same: a developer uses Math.random() because it feels random enough, ships it, and the weakness sits dormant until someone decides to look. By then the tokens are in the wild and the session IDs are predictable. Use the right tool from the start.

Our password generator uses crypto.getRandomValues() through the secureRandomInt() function for every character selection. No Math.random() anywhere in the pipeline. If you're generating a password, that's the minimum standard.

Related Tool

🔐 Password Generator

Generate strong, cryptographically secure passwords.

Try Password Generator