Safeguard
Application Security

Why Math.random() is a security bug waiting to happen

A 2008 Debian OpenSSL patch cut key entropy to ~32,768 values; a 2012 scan found 0.75% of TLS certs shared keys. Weak PRNGs still cause real breaches.

Safeguard Research Team
Research
6 min read

On May 13, 2008, Debian and Ubuntu shipped an advisory, DSA-1571-1, admitting that every SSL, SSH, OpenVPN, and DNSSEC key generated on those systems since September 17, 2006 was guessable. The root cause was a single well-intentioned patch: a maintainer had commented out a line in OpenSSL's entropy-gathering code to silence a Valgrind warning about using uninitialized memory, which happened to be exactly the memory OpenSSL relied on for randomness. The result was a PRNG seeded only by the process ID — a space of roughly 32,768 possible values — meaning every "random" key on affected systems could be brute-forced in minutes. That single line of removed code is the cleanest illustration of a pattern security teams still see today: developers reach for a familiar random-number function without asking whether it is cryptographically safe, and the failure only surfaces after attackers find it first. This post explains why general-purpose PRNGs like Math.random() and rand() are fundamentally unfit for tokens, session IDs, and keys, walks through two real, verified incidents caused by exactly this mistake, and lists the correct CSPRNG API to use in six common languages.

Why is Math.random() insecure for security purposes?

Math.random(), rand(), unseeded java.util.Random, and PHP's mt_rand() all rely on algorithms — typically variants of the Mersenne Twister or a linear congruential generator — built for statistical distribution, not unpredictability. These generators have a finite internal state, and observing a modest number of outputs is enough to reconstruct that state mathematically: Mersenne Twister's full internal state can be recovered from 624 consecutive 32-bit outputs, after which every past and future output becomes computable with certainty. That is disastrous for anything an attacker can sample, like a password-reset token embedded in a URL or a session cookie value. These generators were never designed to resist an adversary studying their outputs; they were designed to pass statistical randomness tests for simulations, games, and sampling, where predictability by a hostile third party was never a threat model.

What actually happened in the Debian OpenSSL incident?

The Debian OpenSSL weak-key incident (CVE-2008-0166) is the canonical entropy failure. The removed line meant OpenSSL's PRNG was seeded almost entirely by the process ID, which on Linux cycles through a small, predictable range — Debian's advisory and Ubuntu's tracking bug (LP #229964) confirmed roughly 32,768 possible seeds per key type and architecture. Anyone could precompute every possible key an affected system might generate and check it against observed public keys. Because the flaw shipped in the OpenSSL package itself for about 20 months before discovery, it affected not just freshly generated SSH host keys but X.509 certificates, DNSSEC keys, and OpenVPN static keys across a huge number of Debian- and Ubuntu-derived servers, forcing a mass key-revocation and regeneration effort industry-wide.

Did weak randomness cause real key compromises beyond Debian?

Yes — and at internet scale. In "Mining Your Ps and Qs" (USENIX Security 2012), Nadia Heninger, Zakir Durumeric, Eric Wustrow, and J. Alex Halderman scanned the public internet's TLS and SSH hosts and found that 0.75% of TLS certificates shared RSA keys with another host, frequently because embedded devices — routers, firewalls, appliances — generated their keys immediately at boot, before the kernel's entropy pool had collected enough randomness. Because two RSA keys sharing one prime factor let you recover both private keys via a simple GCD computation, the researchers factored their way to private keys for an estimated 0.50% of TLS hosts and 0.03% of SSH hosts, and separately recovered DSA private keys for 1.03% of SSH hosts by exploiting weak signature randomness. No malware or credential theft was involved — the keys were simply predictable enough for outsiders to derive.

What should be used instead, language by language?

Every mainstream language ships a cryptographically secure alternative that is a near drop-in replacement. In Node.js and browsers, use crypto.randomBytes(), crypto.randomInt(), or the Web Crypto crypto.getRandomValues() — never Math.random(). In Python, use the secrets module (added specifically for this purpose), not random. In Java, use java.security.SecureRandom instead of java.util.Random. In C and other POSIX code, read from /dev/urandom, call getrandom(2), or use arc4random(). In Go, import crypto/rand, not math/rand. In PHP, use random_bytes() or random_int() rather than rand() or mt_rand(). All of these draw from an OS-level entropy source designed so that observing outputs gives an attacker no practical way to predict others.

How should session tokens actually be generated and handled?

Generate session IDs, CSRF tokens, password-reset tokens, and API keys with at least 128 bits of entropy pulled directly from a CSPRNG, encoded as base64url or hex, with no custom shuffling or truncation logic layered on top — every extra transformation is a chance to quietly reduce entropy. Both incidents above trace back to a developer or maintainer modifying entropy handling with good intentions: silencing a linter warning, or shipping a device that boots before entropy accumulates. The lesson generalizes past cryptography libraries into everyday application code: never re-implement or "improve" how randomness is seeded, never fall back to time-based or PID-based seeds when a CSPRNG call is unavailable, and treat any code review comment reading "this random value looks predictable" as a stop-the-line finding, not a style nitpick.

How Safeguard helps

Insecure randomness is exactly the kind of pattern that hides in plain sight inside large codebases — a single Math.random() call generating a password-reset token buried among hundreds of legitimate uses of the same function for UI jitter or sampling. Safeguard's static analysis pipeline distinguishes these cases by tracing how a random value is actually used: reachability and taint analysis follow a generated value from its source to its sink, so a call to random.random() feeding into a session-token string is flagged as a CWE-330 (Use of Insufficiently Random Values) finding, while the same function used for a non-security calculation is not. Griffin AI then explains the specific data flow and proposes the correct CSPRNG replacement for the language and framework in use, so teams fix the handful of calls that matter instead of auditing every random-number call in the repository by hand.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.