CSPRNG stands for Cryptographically Secure PseudoRandom Number Generator
At Paragon Initiative Enterprises, we believe that security should be the default state of affairs, not something only in the reach of security experts. That is why, in addition to the security consulting services we provide to help companies prevent expensive and embarrassing data breaches (which their customers would otherwise, most likely, be hurt by the most), our team spends a great deal of time working to improve the security of popular free and open source software.
Today, we're pleased to announce an exciting security enhancement coming to WordPress in the next major version.
Starting in 4.4.0,
wp_rand()
is cryptographically secure on all platforms.
The Road to a CSPRNG
Before joining Paragon Initiative Enterprises as its Chief Development Officer, I decided to comb through the code in a lot of popular PHP projects (mostly frameworks, libraries, and content management systems) in my spare time to see if I could find any security vulnerabilities. One of the projects I looked at was WordPress, which currently boasts an adoption rate exceeding 20% of all websites on the Internet.
But before looking at WordPress, I turned a critical eye towards Facebook.
Facebook's Insecure PseudoRandom Number Generator
After a quick review of the Facebook SDK source code on Github, I discovered that, in lieu of using a cryptographically secure pseudorandom number generator, they did this:
$this->state = md5(uniqid(mt_rand(), true));
In a perfect world, this provides, at most, 61 bits of entropy (32 from mt_rand()
, and only 29 bits from uniqid()
). Given that an attacker can perform an exhaustive 56-bit brute force in 24 hours, and each bit doubles the time required, this means that an attacker only needs 32 days to calculate every possible output of this function, which they could store and search at their convenience.
Such a database would require several petabytes of storage space, but would allow a sophisticated attacker to recover the mt_rand
seed and predict the next RNG output.
I quickly wrote and submitted a pull request proposing the adoption of a CSPRNG. The initial reaction was, "Considering that using uniqid()
and mt_rand()
are a pretty dang good one-liner, I'm not convinced we need to complicate the code in the SDK with random number generation," followed by, "Thanks for the interest, but I don't believe it's necessary to change this for everyone in the SDK," and my pull request was closed.
Yikes! Facebook rejected a patch that improved security for all of its countless SDK users? Not if I can help it. I continued the discussion, citing research from the 2012 Black Hat Briefings on cracking pseudo-random number generators and emphasized the importance of getting it right for two main reasons:
- Lots of developers will depend on this for their applications to be secure.
- A nontrivial percentage of developers will learn how to program PHP at least in part from seeing how the SDK works. By providing a better example to learn from, Facebook could prevent the spread of bad security habits and instead diffuse better security practices throughout new developers.
They listened. My patch was merged and later refined by their team. Facebook's SDK has only improved since then.
Epilogue: The person who said that "using uniqid()
and mt_rand()
[is] a pretty dang good one-liner" was Sammy Kaye Powers, who later went on to help write the RFC and patch to introduce random_bytes()
and random_int()
in PHP 7.
Meanwhile, Back at the Ranch WordPress
The following day, I decided to look at WordPress and discovered that they, too, eschewed a CSPRNG in favor of a hacky workaround, although theirs was stateful. Outside race conditions on the database, it's difficult to even conceptualize an attack, let alone perform one.
Nonetheless, some of the WordPress developers agreed that switching to a CSPRNG would be great, but there was one problem: WordPress still supported PHP 5.2.4 and it's very difficult to get cryptographically secure randomness on PHP 5.2. As a result of portability and backward compatibility concerns (and one of the WordPress core developers not receiving my emails), the discussion stalled.
Enter random_compat
After publishing a blog post earlier this year about how to generate random data in PHP, Sammy Kaye Powers looped me into a discussion about the possibility of writing a userland PHP 5 compatibility library for the new functions offered by PHP 7. Seeing as this was a specific topic I was rather passionate about, and had studied in great detail, I volunteered to write it (after checking with the CEO, of course). And thus began the development on paragonie/random_compat.
In the course of making sure that random_compat
was as secure (and true to the PHP 7 implementation) as possible, I solicited the advice and criticism of many other PHP security experts, insisting that this was something that we intended to benefit the entire community. To my delight, many of them responded. Pretty soon, even reputed cryptography experts like Christian Winnerlein (one of the BLAKE2 developers) and Frank Denis (author of libsodium) both chimed in with advice and suggestions. In a very short period of time, random_compat
transformed from "something Scott put together that tries as hard as it can to be secure" to "almost certainly the most secure random number generator in PHP 5," if by no other metric than the amount of independent review it received from the community.
This is nothing short of incredible.
Through the course of discussing specific design decisions, another user (Mason Malone) discovered a pretty interesting problem in openssl_random_pseudo_bytes()
, which was explored further in detail by a PHP core contributor and later filed as PHP bug #70014.
This would only be the first time that attempting to use a feature correctly in random_compat
would lead to a security enhancement in the PHP core. Not long after, Andrey Andreev opened an issue asking if it was truly a compatibility library, citing PHP 7's behavior in the rare and unfortunate event of a critical CSPRNG failure.
/**
* PHP 7.0.0 before RC3:
*
* If a critical RNG failure happens, this returns the string
* aaaaaaaaaaaa in addition to raising 12 E_WARNING exceptions.
* Not good!
*/
function genPassword($length = 12, $alphabet = 'abcdefghijklmnopqrstuvwxyz')
{
$password = '';
$max = strlen($alphabet) - 1;
for ($i = 0; $i < $length; ++$i) {
$password .= $alphabet[random_int(0, $max)];
}
return $password;
}
After months of discussion and an emergency RFC/vote, as of PHP 7.0.0-RC3, these functions will throw an Exception
or Error
object instead of returning false.
/**
* PHP 7.0.0 since RC3:
*
* If a critical RNG failure happens, random_int() will throw an Exception.
* If it's misused, it will throw an Error or TypeError.
*/
try {
$password = genPassword(
16,
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
);
} catch (Exception $e) {
$framework->view->render(
'500.twig',
'An unexpected error has occurred. Please try again. Sorry.'
);
}
And with that, we were able to tag the 1.0.0
stable release of random_compat
.
Random_compat Matures, Gets Adopted
In the time since we tagged 1.0.0, several people have contributed minor enhancements and optimizations. With a highly reliable and well-studied forward-compatible CSPRNG polyfill library for PHP 5 projects publicly available, we resurrected the discussion to introduce a CSPRNG into WordPress. Dion Hulse, the core WordPress contributor who had been assigned to my Trac ticket from well over a year ago, approved the notion and pledged to review it, while setting its milestone to 4.4 (the next WordPress version release).
With that discussion underway, I reached out to two popular frameworks, Laravel and Symfony, and asked if they would be interested in adopting random_compat
, which was forward-compatible with PHP 7's random_bytes()
and random_int()
functions and worked all the way back to PHP 5.2.x. Both framework's teams agreed that this was a good move. The next versions of Symfony and Laravel now require it as a dependency.
I also reached out to CodeIgniter, but they announced that CodeIgniter 4 would require PHP 7, so it was a non-starter.
In parallel, Dion Hulse reported back that one of the recent changes to random_compat
introduced an unexpected error on PHP 5.2. I quickly fixed the problem and released version 1.0.5
of random_compat
. With that error fixed, random_compat
is now slated to be implemented (or recommended) in:
- Laravel 5.2
- PHPixie 3
- Symfony 2.8
- WordPress 4.4
- Possibly others that I'm not yet aware of
Going Forward
Our part in this long and crazy journey has reached its end. In the course of fixing the same flaw in two distinct projects, the PHP community banded together to identify and expunge a bug in the PHP core, create a new feature in PHP 7, and in some small way helped to secure the CMS that powers more than 20% of websites on the Internet.
If you feel grateful, please share your well-wishes with the contributors listed on the random_compat README. Without their hard work, I might still be writing emails and Github comments arguing about the importance of a good CSPRNG and feeling like nobody cares and nothing would have been fixed.
If you would like to join in next year's push to make PHP more secure, take a look at Paragon Initiative's plans to improve cryptography in PHP 7.1 and let us know (either via email or via Twitter).
And, as always, if you're the decision-maker for a company interested in making the software you use or produce more secure, even against sophisticated threats, get in touch. We specialize in application security and applied cryptography.
If you'd like to learn more about the quality of service we offer, check out our other open source software projects.