From 0e322469a646f7fcb9c9884370aec1b40e663bcf Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Thu, 8 Oct 2015 00:02:24 +0000 Subject: [PATCH] Use PHP7's `random_int()` CSPRNG functionality in `wp_rand()` with a fallback to the `random_compat` library for PHP 5.x. `random_compat` offers a set of compatible functions for older versions of PHP, filling in the gap by using other PHP extensions when available. We still include our existing `wp_rand()` functionality as a fallback for when no proper CSPRNG exists on the system. Props sarciszewski See #28633 Built from https://develop.svn.wordpress.org/trunk@34922 git-svn-id: http://core.svn.wordpress.org/trunk@34887 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/compat.php | 5 + wp-includes/pluggable.php | 33 +++- .../random_compat/byte_safe_strings.php | 154 ++++++++++++++++ wp-includes/random_compat/error_polyfill.php | 56 ++++++ wp-includes/random_compat/random.php | 83 +++++++++ .../random_compat/random_bytes_com_dotnet.php | 75 ++++++++ .../random_bytes_dev_urandom.php | 135 ++++++++++++++ .../random_compat/random_bytes_mcrypt.php | 70 +++++++ .../random_compat/random_bytes_openssl.php | 74 ++++++++ wp-includes/random_compat/random_int.php | 172 ++++++++++++++++++ wp-includes/version.php | 2 +- 11 files changed, 855 insertions(+), 4 deletions(-) create mode 100644 wp-includes/random_compat/byte_safe_strings.php create mode 100644 wp-includes/random_compat/error_polyfill.php create mode 100644 wp-includes/random_compat/random.php create mode 100644 wp-includes/random_compat/random_bytes_com_dotnet.php create mode 100644 wp-includes/random_compat/random_bytes_dev_urandom.php create mode 100644 wp-includes/random_compat/random_bytes_mcrypt.php create mode 100644 wp-includes/random_compat/random_bytes_openssl.php create mode 100644 wp-includes/random_compat/random_int.php diff --git a/wp-includes/compat.php b/wp-includes/compat.php index 781e7a728f..24da0adc3e 100644 --- a/wp-includes/compat.php +++ b/wp-includes/compat.php @@ -332,3 +332,8 @@ if ( ! interface_exists( 'JsonSerializable' ) ) { public function jsonSerialize(); } } + +// random_int was introduced in PHP 7.0 +if ( ! function_exists( 'random_int' ) ) { + require ABSPATH . WPINC . '/random_compat/random.php'; +} diff --git a/wp-includes/pluggable.php b/wp-includes/pluggable.php index d58f18940b..36e330be3a 100644 --- a/wp-includes/pluggable.php +++ b/wp-includes/pluggable.php @@ -2132,9 +2132,11 @@ if ( !function_exists('wp_rand') ) : * Generates a random number * * @since 2.6.2 + * @since 4.4 Uses PHP7 random_int() or the random_compat library if avaialble. * * @global string $rnd_value * @staticvar string $seed + * @staticvar bool $external_rand_source_available * * @param int $min Lower limit for the generated number * @param int $max Upper limit for the generated number @@ -2143,6 +2145,34 @@ if ( !function_exists('wp_rand') ) : function wp_rand( $min = 0, $max = 0 ) { global $rnd_value; + // Some misconfigured 32bit environments (Entropy PHP, for example) truncate integers larger than PHP_INT_MAX to PHP_INT_MAX rather than overflowing them to floats. + $max_random_number = 3000000000 === 2147483647 ? (float) "4294967295" : 4294967295; // 4294967295 = 0xffffffff + + // We only handle Ints, floats are truncated to their integer value. + $min = (int) $min; + $max = (int) $max; + + // Use PHP's CSPRNG, or a compatible method + static $use_random_int_functionality = true; + if ( $use_random_int_functionality ) { + try { + $_max = ( 0 != $max ) ? $max : $max_random_number; + // wp_rand() can accept arguements in either order, PHP cannot. + $_max = max( $min, $_max ); + $_min = min( $min, $_max ); + $val = random_int( $_min, $_max ); + if ( false !== $val ) { + return absint( $val ); + } else { + $use_random_int_functionality = false; + } + } catch ( Throwable $t ) { + $use_random_int_functionality = false; + } catch ( Exception $e ) { + $use_random_int_functionality = false; + } + } + // Reset $rnd_value after 14 uses // 32(md5) + 40(sha1) + 40(sha1) / 8 = 14 random numbers from $rnd_value if ( strlen($rnd_value) < 8 ) { @@ -2167,9 +2197,6 @@ function wp_rand( $min = 0, $max = 0 ) { $value = abs(hexdec($value)); - // Some misconfigured 32bit environments (Entropy PHP, for example) truncate integers larger than PHP_INT_MAX to PHP_INT_MAX rather than overflowing them to floats. - $max_random_number = 3000000000 === 2147483647 ? (float) "4294967295" : 4294967295; // 4294967295 = 0xffffffff - // Reduce the value to be within the min - max range if ( $max != 0 ) $value = $min + ( $max - $min + 1 ) * $value / ( $max_random_number + 1 ); diff --git a/wp-includes/random_compat/byte_safe_strings.php b/wp-includes/random_compat/byte_safe_strings.php new file mode 100644 index 0000000000..c6dc2a865a --- /dev/null +++ b/wp-includes/random_compat/byte_safe_strings.php @@ -0,0 +1,154 @@ +GetRandom() + * 4. openssl_random_pseudo_bytes() + * + * See ERRATA.md for our reasoning behind this particular order + */ + if (!ini_get('open_basedir') && is_readable('/dev/urandom')) { + // See random_bytes_dev_urandom.php + require_once "random_bytes_dev_urandom.php"; + } elseif (PHP_VERSION_ID >= 50307 && function_exists('mcrypt_create_iv')) { + // See random_bytes_mcrypt.php + require_once "random_bytes_mcrypt.php"; + } elseif (extension_loaded('com_dotnet')) { + // See random_bytes_com_dotnet.php + require_once "random_bytes_com_dotnet.php"; + } elseif (function_exists('openssl_random_pseudo_bytes')) { + // See random_bytes_openssl.php + require_once "random_bytes_openssl.php"; + } else { + /** + * We don't have any more options, so let's throw an exception right now + * and hope the developer won't let it fail silently. + */ + function random_bytes() + { + throw new Exception( + 'There is no suitable CSPRNG installed on your system' + ); + } + } + } + if (!function_exists('random_int')) { + require_once "random_int.php"; + } +} diff --git a/wp-includes/random_compat/random_bytes_com_dotnet.php b/wp-includes/random_compat/random_bytes_com_dotnet.php new file mode 100644 index 0000000000..cebe616ab2 --- /dev/null +++ b/wp-includes/random_compat/random_bytes_com_dotnet.php @@ -0,0 +1,75 @@ +GetRandom($bytes, 0)); + if (RandomCompat_strlen($buf) >= $bytes) { + /** + * Return our random entropy buffer here: + */ + return RandomCompat_substr($buf, 0, $bytes); + } + ++$execCount; + } while ($execCount < $bytes); + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'PHP failed to generate random data.' + ); +} diff --git a/wp-includes/random_compat/random_bytes_dev_urandom.php b/wp-includes/random_compat/random_bytes_dev_urandom.php new file mode 100644 index 0000000000..09bd49f551 --- /dev/null +++ b/wp-includes/random_compat/random_bytes_dev_urandom.php @@ -0,0 +1,135 @@ + 0); + + /** + * Is our result valid? + */ + if ($buf !== false) { + if (RandomCompat_strlen($buf) === $bytes) { + /** + * Return our random entropy buffer here: + */ + return $buf; + } + } + } + /** + * If we reach here, PHP has failed us. + */ + throw new Exception( + 'PHP failed to generate random data.' + ); +} diff --git a/wp-includes/random_compat/random_bytes_mcrypt.php b/wp-includes/random_compat/random_bytes_mcrypt.php new file mode 100644 index 0000000000..8d3b728371 --- /dev/null +++ b/wp-includes/random_compat/random_bytes_mcrypt.php @@ -0,0 +1,70 @@ + $max) { + throw new Error( + 'Minimum value must be less than or equal to the maximum value' + ); + } + if ($max === $min) { + return $min; + } + + /** + * Initialize variables to 0 + * + * We want to store: + * $bytes => the number of random bytes we need + * $mask => an integer bitmask (for use with the &) operator + * so we can minimize the number of discards + */ + $attempts = $bits = $bytes = $mask = $valueShift = 0; + + /** + * At this point, $range is a positive number greater than 0. It might + * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to + * a float and we will lose some precision. + */ + $range = $max - $min; + + /** + * Test for integer overflow: + */ + if (!is_int($range)) { + /** + * Still safely calculate wider ranges. + * Provided by @CodesInChaos, @oittaa + * + * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435 + * + * We use ~0 as a mask in this case because it generates all 1s + * + * @ref https://eval.in/400356 (32-bit) + * @ref http://3v4l.org/XX9r5 (64-bit) + */ + $bytes = PHP_INT_SIZE; + $mask = ~0; + } else { + /** + * $bits is effectively ceil(log($range, 2)) without dealing with + * type juggling + */ + while ($range > 0) { + if ($bits % 8 === 0) { + ++$bytes; + } + ++$bits; + $range >>= 1; + $mask = $mask << 1 | 1; + } + $valueShift = $min; + } + + /** + * Now that we have our parameters set up, let's begin generating + * random integers until one falls between $min and $max + */ + do { + /** + * The rejection probability is at most 0.5, so this corresponds + * to a failure probability of 2^-128 for a working RNG + */ + if ($attempts > 128) { + throw new Exception( + 'random_int: RNG is broken - too many rejections' + ); + } + + /** + * Let's grab the necessary number of random bytes + */ + $randomByteString = random_bytes($bytes); + if ($randomByteString === false) { + throw new Exception( + 'Random number generator failure' + ); + } + + /** + * Let's turn $randomByteString into an integer + * + * This uses bitwise operators (<< and |) to build an integer + * out of the values extracted from ord() + * + * Example: [9F] | [6D] | [32] | [0C] => + * 159 + 27904 + 3276800 + 201326592 => + * 204631455 + */ + $val = 0; + for ($i = 0; $i < $bytes; ++$i) { + $val |= ord($randomByteString[$i]) << ($i * 8); + } + + /** + * Apply mask + */ + $val &= $mask; + $val += $valueShift; + + ++$attempts; + /** + * If $val overflows to a floating point number, + * ... or is larger than $max, + * ... or smaller than $int, + * then try again. + */ + } while (!is_int($val) || $val > $max || $val < $min); + return (int) $val; +} diff --git a/wp-includes/version.php b/wp-includes/version.php index 2c2d209929..66d6ccc671 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.4-alpha-34921'; +$wp_version = '4.4-alpha-34922'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.