In 2008, CVE-2008-4107 documented something that should have been obvious years earlier: PHP's rand() and mt_rand() functions do not produce cryptographically strong output, and applications like Joomla 1.5.x and WordPress before 2.6.2 were using them to generate password-reset tokens. An attacker who could observe a handful of outputs — from a CAPTCHA, a session ID, or a leaked token — could reconstruct the Mersenne Twister seed and predict every future value, including the reset token for any account on the system. It took PHP until version 7.0, released December 3, 2015, to ship a first-class answer: random_bytes() and random_int(), functions built specifically to produce cryptographically secure randomness by pulling directly from the operating system's CSPRNG. Nearly a decade later, misuse of weak randomness is still one of the more common findings in PHP codebases we scan. Here's why it matters and how to get it right.
Why isn't rand() or mt_rand() safe for security-sensitive code?
Because both functions are built on pseudo-random number generators designed for speed and statistical distribution, not unpredictability. rand() in older PHP versions wrapped the C library's LCG (linear congruential generator), and mt_rand() uses the Mersenne Twister algorithm — both are fully deterministic once you know the seed, and the seed space is small enough to brute-force. The Mersenne Twister implementation in PHP seeds from a 32-bit integer, meaning there are at most about 4.29 billion (2^32) possible internal states. Tools like php_mt_seed, maintained by Openwall, and academic research such as the 2012 USENIX Security paper "I Forgot Your Password: Randomness Attacks Against PHP Applications" showed that recovering the seed from just two or three observed outputs, then bruteforcing the remaining search space, is a matter of seconds to minutes on ordinary hardware — no supercomputer required. Once an attacker has the seed, they can reconstruct every value mt_rand() has produced or will produce in that process, including tokens for CSRF protection, password resets, API keys, or "unguessable" upload URLs. This is precisely the class of weakness tracked as CWE-338 (Use of Cryptographically Weak PRNG) in vulnerability databases, and it's why security guidance — from OWASP to PHP's own manual — explicitly warns against rand(), mt_rand(), array_rand(), uniqid(), and shuffle() for anything security-relevant.
What changed when PHP 7.0 introduced random_bytes() and random_int()?
PHP 7.0 gave every developer a cryptographically secure source of randomness without needing an extension or a third-party library. The functions were formalized through an RFC championed by Anthony Ferrara and shipped as part of core, with random_bytes($length) returning raw CSPRNG bytes and random_int($min, $max) returning an unbiased integer in a given range — both suitable for session tokens, password reset codes, API keys, and cryptographic key material. Before this, developers on PHP 5.x had to reach for openssl_random_pseudo_bytes() (which had its own history of returning non-cryptographically-strong output on certain platforms if OpenSSL wasn't configured correctly) or install paragonie/random_compat, a userland polyfill released in 2015 by Paragon Initiative Enterprises that implemented the same random_bytes()/random_int() API on top of the best available source for PHP 5.2–5.6. The practical effect of the PHP 7.0 change was significant: within weeks, WordPress adopted the new functions in its wp_rand() wrapper starting with version 4.4.0 in December 2015, so any site running PHP 7 automatically inherited OS-level CSPRNG output instead of a homegrown mix of mt_rand(), timestamps, and hashes.
How does random_bytes() actually source its entropy?
It delegates entirely to the operating system's cryptographically secure random number generator rather than implementing its own. On Linux, PHP calls the getrandom(2) syscall (available since kernel 3.17, released in 2014) when present, falling back to reading from /dev/urandom otherwise. On BSD-derived systems and macOS, it uses arc4random_buf(). On Windows, it calls CryptGenRandom() (or BCryptGenRandom() on newer builds). These are the same primitives operating systems use to generate TLS session keys and disk encryption keys — they're seeded from hardware entropy sources (interrupt timing, thermal noise, dedicated RNG instructions like RDRAND) and are designed to remain unpredictable even if an attacker knows the algorithm and has observed prior output. This is the core distinction from mt_rand(): there is no small, guessable internal seed to recover, because the generator's state is continuously reseeded by the kernel from hardware entropy rather than initialized once from a 32-bit value at process start.
What happens when random_bytes() fails, and how should you handle it?
It throws an Exception (or Error in some failure modes) rather than silently returning weak or predictable data, and that behavior needs to be handled explicitly, not swallowed. Unlike mt_rand(), which always returns something even if the underlying entropy is questionable, random_bytes() is designed to fail loudly if it cannot guarantee cryptographic strength — for example, if /dev/urandom is unreadable in a locked-down container, or a Windows API call fails. We regularly see code in the wild that wraps random_bytes() in a try/catch and falls back to mt_rand() or str_shuffle() "just to keep things working," which quietly reintroduces the exact vulnerability class the function exists to eliminate. The correct pattern is to let the exception propagate (or fail the request with a 500) rather than degrade to insecure randomness, and to alert on it — a production server that can't source CSPRNG output has a bigger problem than one failed token generation. It's also worth noting that random_bytes() returns raw binary; if you need a URL-safe string, encode it with bin2hex() or base64_encode()/rtrim(strtr(...)) rather than reaching for uniqid(), which is based on the system clock (microtime) and has no cryptographic value at all despite superficially "looking random."
What real-world breaches show the cost of getting this wrong?
CVE-2008-4107 is the canonical example, but the pattern has repeated across the PHP ecosystem for over a decade because the fix requires developers to know the difference between "random-looking" and "cryptographically random." The 2012 USENIX paper referenced above catalogued multiple production CMS and framework instances where mt_rand()-derived values secured password resets, CSRF tokens, and API keys — and demonstrated working exploits against several of them using seed-recovery techniques. More recent research, including a 2024 writeup on exploiting mt_rand() internals, shows the underlying weakness hasn't gone away even as the language-level fix has existed since 2015; it shows up in legacy code, in third-party libraries that were never updated, and in code copied from outdated tutorials. Snowflake, a public tool built specifically to automate mt_rand()/rand() seed recovery in PHP applications, exists precisely because this vulnerability class keeps recurring in new codebases. The lesson for engineering teams is that this isn't a historical curiosity fixed once in 2015 — it's a class of bug that gets reintroduced every time a developer reaches for the familiar rand()/mt_rand() functions out of habit, unaware that PHP has had a purpose-built, equally simple alternative for over ten years.
How Safeguard Helps
Safeguard scans PHP codebases and their dependency trees for exactly this class of weakness — calls to rand(), mt_rand(), uniqid(), and array_rand() in security-sensitive contexts like token generation, password reset flows, API key issuance, and session handling — and flags them before they ship. Because weak randomness is a silent bug (the code runs fine, tests pass, and nothing looks wrong until someone runs php_mt_seed against your token output), it's the kind of finding that's easy to miss in manual review but straightforward to catch with static analysis tuned to these patterns. Safeguard also tracks these findings across your software supply chain, not just your first-party code: a random_bytes()-based token generator in your application doesn't help if a transitive dependency is still minting session identifiers with uniqid(). For teams managing SOC 2 or similar compliance obligations, Safeguard maps these findings to the relevant control families (cryptographic controls, access provisioning) so remediation work maps cleanly to audit evidence rather than becoming a one-off scramble. The fix itself is rarely the hard part — swapping mt_rand() for random_int() is a one-line change — the hard part is finding every place it's needed across a codebase and its dependencies, which is exactly the gap continuous scanning is built to close.