Nothing is truly random

Random thoughts about wp_rand()

Security | WordPress

I was reviewing a code audit for a client recently. It’s not unusual, clients sometimes get automated scans or audits done by third parties who aren’t really WordPress specialists, and they end up with a giant, indecipherable report. I usually get asked to go through it and see if what’s being reported actually makes sense.

This particular report had flagged a CRITICAL issue. Yes, critical. Apparently, and I quote, it “requires immediate attention and could lead to imminent issues.” The big scary problem? The code was using wp_rand() and it needed to be replaced immediately.

Right. That’s a bit of a strange statement to make, but before we get into that, let’s talk about what wp_rand() actually is and how it works.

What is wp_rand()?

wp_rand() is WordPress’s version of PHP’s random number function. When you call it, WordPress first tries to use PHP’s built-in random_int(), which pulls unpredictable numbers straight from your operating system, the same secure source used for encryption keys and all sorts of cryptographic operations.

If, for some reason, your server can’t use random_int() (maybe you’re running an ancient version of PHP or something’s misconfigured), WordPress falls back to a mix of mt_rand(), timestamps, and hashes. So, if you’re using PHP 7 or above, which let’s be honest, you absolutely should be, then wp_rand() will use random_int() which is getting its source from your operating system CSPRNG.

What’s a CSPRNG?

A cryptographically secure pseudo-random number generator (CSPRNG) is a system designed to generate numbers that are unpredictable.

It’s not enough for numbers to just look random. To count as cryptographically secure, the generator has to pass two main tests:

  1. Unpredictability (next-bit test) — Even if you’ve seen a long sequence of output, you can’t guess the next number any better than flipping a coin.
  2. Backtracking resistance — If someone somehow learns the internal state of the generator, they still can’t rewind and reconstruct everything it’s produced so far.

Sounds simple, right? Except it’s not. There’s a lot going on under the hood. Functions like random_int() don’t actually do the randomness themselves; they rely on your operating system’s internal APIs such as /dev/urandom on Linux systems to handle that.

To put it simply, wp_rand() uses random_int(), which uses your server’s OS-level API to create random numbers.

So, we’re good, right? The report is just wrong, and we can close it as “won’t fix”?

Well…

The fallback path

If you’re not on PHP 7+, then WordPress can’t use random_int() and instead falls back to the older pseudo-random setup. It tries random_int() and, if it fails to get an integer back, errors and defaults to its older, less secure method.

So, could a bad actor somehow force that fallback? Or stop random_int() from working?

Let’s think it through:

  • Run an older version of PHP. If someone somehow forced a downgrade of PHP (which would already be a red flag), then yes, you’d lose access to random_int(). Pretty unlikely, but maybe possible if your host had some exposed API.
  • Disable random_int() altogether. You could mess with PHP configuration files like php.ini to block it — but if someone has that level of access, they can probably already read wp-config.php and therefore have database credentials.
  • Block /dev/urandom. This would be the sneaky one. /dev/urandom is where the system (on Linux) pulls its randomness from. But again, if you could tamper with that, you’re basically controlling the entire randomness pool and at that point, you could just make it always return “1” if you wanted!

So yes, it’s technically possible to stop random_int() being used, but it’s very, very unlikely unless your system is already misconfigured or compromised.

Following the unlikely scenario

Let’s say, for the sake of argument, the bad actor has somehow pulled that off and they’re not already doing something more catastrophic. Is the fallback code still OK in normal use?

The fallback grabs a few things the random_seed transient, timestamps, and then mt_rand() for good measure.

    /*
		 * Reset $rnd_value after 14 uses.
		 * 32 (md5) + 40 (sha1) + 40 (sha1) / 8 = 14 random numbers from $rnd_value.
		 */
		if ( strlen( $rnd_value ) < 8 ) {
			if ( defined( 'WP_SETUP_CONFIG' ) ) {
				static $seed = '';
			} else {
				$seed = get_transient( 'random_seed' );
			}
			$rnd_value  = md5( uniqid( microtime() . mt_rand(), true ) . $seed );
			$rnd_value .= sha1( $rnd_value );
			$rnd_value .= sha1( $rnd_value . $seed );
			$seed       = md5( $seed . $rnd_value );
			if ( ! defined( 'WP_SETUP_CONFIG' ) && ! defined( 'WP_INSTALLING' ) ) {
				set_transient( 'random_seed', $seed );
			}
		}

		// Take the first 8 digits for our value.
		$value = substr( $rnd_value, 0, 8 );

		// Strip the first eight, leaving the remainder for the next call to wp_rand().
		$rnd_value = substr( $rnd_value, 8 );

		$value = abs( hexdec( $value ) );

		// Reduce the value to be within the min - max range.
		$value = $min + ( $max - $min + 1 ) * $value / ( $max_random_number + 1 );

		return abs( (int) $value );
		

The two interesting bits here are the transient (which could be influenced if someone already had access) and mt_rand().

So what’s mt_rand() then?

mt_rand() is PHP’s older pseudo-random number generator, based on the Mersenne Twister algorithm. It’s deterministic, meaning it produces the same sequence of “random” numbers if you give it the same seed. It uses a 32-bit internal state and cycles through a very long period (2³¹−1) before repeating. It’s fast and statistically uniform, but importantly not cryptographically secure.

So, if we knew the seed, we could predict the output. The question is where does that seed come from?

If you’re using PHP 7+, the seed itself also comes from /dev/urandom, so even in our “everything’s gone wrong” scenario, we should still ultimately be relying on the server’s random source.

But in PHP 5.6 and earlier, the seed was generated using PHP’s own internal logic,  specifically php_combined_lcg(), which mixes in process-specific data like microsecond timing and process ID. That means, in theory, you could brute-force it and it is not cryptographically secure.

How much brute force are we talking about?

Alright, some rough maths. If you can see a string generated by wp_rand(), you can think of it as working with 32-bit slices (8 hex characters) per call. Brute-forcing a single 32-bit value means trying around 2³² possibilities out of about 4.3 billion attempts.

If each attempt requires an HTTP request, that’s roughly 49,710 requests per second to have a shot at brute-forcing it in a reasonable timeframe. Is that doable? Maybe. 

Will your server happily handle 50,000 requests per second to an uncached endpoint? Probably not.

So yes, there’s a theoretical risk that wp_rand() could be compromised but to pull it off,a bunch of things are either severely misconfigured or the bad actor already has the capability to compromise the system..

Is that a critical issue? 

No. Worth noting? Maybe, if you’re really scraping the barrel for security findings.

But what if your compliance team insists?

If you’re stuck in a situation where someone higher up insists on fixing it, you can override it. wp_rand() lives in pluggable.php. Functions in that file are wrapped in “if function doesn’t already exist” checks, so plugins can override them. 

You could, in theory, write your own version that always calls random_int() and never falls back.

function wp_rand( $min = null, $max = null ) {
	$platform_max = PHP_INT_SIZE === 4 ? (int) 0x7FFFFFFF : PHP_INT_MAX;

	if ( null === $min ) {
		$min = 0;
	}

	if ( null === $max ) {
		$max = $platform_max;
	}

	$min = (int) $min;
	$max = (int) min( $max, $platform_max );

	if ( $min > $max ) {
		_doing_it_wrong(
			__FUNCTION__,
			'$min must not exceed $max.',
			'1.0.0'
		);

		$temp = $min;
		$min  = $max;
		$max  = $temp;
	}

	return random_int( $min, $max );
}

That would also let you handle systems that are greater than 32-bit.

Would I recommend it? No. 

The current function is fine. That’s why no one’s touched it in years.

While it technically introduces a tiny risk in some very specific circumstances, replacing it with your own code just gives you one more thing to maintain and that’s a much bigger attack surface overall.

Helping you and your customers stay safe


WordPress Security Consulting Services

Power Hour Consulting

Want to get expert advice on your site's security? Whether you're dealing with a hacked site or looking to future-proof your security, Tim will provide personalised guidance and answer any questions you may have. A power hour call is an ideal starting place for a project or a way to break deadlocks in complex problems.

Learn more

Site Reviews

Want to feel confident about your site's security and performance? A website review from Tim has got you covered. Using a powerful combination of automated and manual testing to analyse your site for any potential vulnerabilities or performance issues. With a comprehensive report and, importantly, recommendations for each action required.

Learn more

Code Reviews

Is your plugin or theme code secure and performing at its best? Tim provides a comprehensive code review, that combine the power of manual and automated testing, as well as a line-by-line analysis of your code base. With actionable insights, to help you optimise your code's security and performance.

Learn more

Or let's chat about your security?

Book a FREE 20 minute call with me to see how you can improve your WordPress Security.

(No Strings Attached, honest!)