diff --git a/composer.lock b/composer.lock index 832979fc30d1b56a0a38a437dd8284f1e31e8ed3..2ecd5c2e172122cd17f4c583a3010e622a355962 100644 --- a/composer.lock +++ b/composer.lock @@ -999,16 +999,16 @@ }, { "name": "paragonie/random_compat", - "version": "1.1.1", + "version": "v2.0.2", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "a208865a5aeffc2dbbef2a5b3409887272d93f32" + "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/a208865a5aeffc2dbbef2a5b3409887272d93f32", - "reference": "a208865a5aeffc2dbbef2a5b3409887272d93f32", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/088c04e2f261c33bed6ca5245491cfca69195ccf", + "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf", "shasum": "" }, "require": { @@ -1043,7 +1043,7 @@ "pseudorandom", "random" ], - "time": "2015-12-01 02:52:15" + "time": "2016-04-03 06:00:07" }, { "name": "psr/http-message", diff --git a/core/composer.json b/core/composer.json index 53ab8fa68aa36c37844cfadd3063752a0f46dc6a..ba488e5d2e9760299b8f5f582279f0256cc86ca0 100644 --- a/core/composer.json +++ b/core/composer.json @@ -31,7 +31,7 @@ "symfony/psr-http-message-bridge": "v0.2", "zendframework/zend-diactoros": "~1.1", "composer/semver": "~1.0", - "paragonie/random_compat": "~1.0", + "paragonie/random_compat": "^1.0|^2.0", "asm89/stack-cors": "~1.0" }, "require-dev": { diff --git a/core/lib/Drupal/Component/Utility/Crypt.php b/core/lib/Drupal/Component/Utility/Crypt.php index ace4ebda1a9a12a84fd1ab1bfc48518e09d48a12..6ebdc4aac831b4cd4824417d6e3f9be452470322 100644 --- a/core/lib/Drupal/Component/Utility/Crypt.php +++ b/core/lib/Drupal/Component/Utility/Crypt.php @@ -19,7 +19,8 @@ class Crypt { * * In PHP 7 and up, this uses the built-in PHP function random_bytes(). * In older PHP versions, this uses the random_bytes() function provided by - * the random_compat library. + * the random_compat library, or the fallback hash-based generator from Drupal + * 7.x. * * @param int $count * The number of characters (bytes) to return in the string. @@ -28,7 +29,43 @@ class Crypt { * A randomly generated string. */ public static function randomBytes($count) { - return random_bytes($count); + try { + return random_bytes($count); + } + catch (\Exception $e) { + // $random_state does not use drupal_static as it stores random bytes. + static $random_state, $bytes; + // If the compatibility library fails, this simple hash-based PRNG will + // generate a good set of pseudo-random bytes on any system. + // Note that it may be important that our $random_state is passed + // through hash() prior to being rolled into $output, that the two hash() + // invocations are different, and that the extra input into the first one + // - the microtime() - is prepended rather than appended. This is to avoid + // directly leaking $random_state via the $output stream, which could + // allow for trivial prediction of further "random" numbers. + if (strlen($bytes) < $count) { + // Initialize on the first call. The $_SERVER variable includes user and + // system-specific information that varies a little with each page. + if (!isset($random_state)) { + $random_state = print_r($_SERVER, TRUE); + if (function_exists('getmypid')) { + // Further initialize with the somewhat random PHP process ID. + $random_state .= getmypid(); + } + $bytes = ''; + // Ensure mt_rand() is reseeded before calling it the first time. + mt_srand(); + } + + do { + $random_state = hash('sha256', microtime() . mt_rand() . $random_state); + $bytes .= hash('sha256', mt_rand() . $random_state, TRUE); + } while (strlen($bytes) < $count); + } + $output = substr($bytes, 0, $count); + $bytes = substr($bytes, $count); + return $output; + } } /** diff --git a/core/lib/Drupal/Component/Utility/composer.json b/core/lib/Drupal/Component/Utility/composer.json index a23634facfd671dedbf0786fa58598f28f2dfa5f..13671efef403430f1e3c0f90779928018b8c2549 100644 --- a/core/lib/Drupal/Component/Utility/composer.json +++ b/core/lib/Drupal/Component/Utility/composer.json @@ -6,7 +6,7 @@ "license": "GPL-2.0+", "require": { "php": ">=5.5.9", - "paragonie/random_compat": "~1.0", + "paragonie/random_compat": "^1.0|^2.0", "drupal/core-render": "~8.2" }, "autoload": { diff --git a/core/modules/system/system.install b/core/modules/system/system.install index cbdafbc9f436bb6bebca67f1d89d8d63bdf570fc..e8f45eb445acfb1ba03370524d7cc9514bd050ea 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -257,6 +257,44 @@ function system_requirements($phase) { $requirements['php_opcache']['title'] = t('PHP OPcode caching'); } + if ($phase != 'update') { + // Test whether we have a good source of random bytes. + $requirements['php_random_bytes'] = array( + 'title' => t('Random number generation'), + ); + try { + $bytes = random_bytes(10); + if (strlen($bytes) != 10) { + throw new \Exception(t('Tried to generate 10 random bytes, generated @count', array('@count' => strlen($bytes)))); + } + $requirements['php_random_bytes']['value'] = t('Successful'); + } + catch (\Exception $e) { + // If /dev/urandom is not available on a UNIX-like system, check whether + // open_basedir restrictions are the cause. + $open_basedir_blocks_urandom = FALSE; + if (DIRECTORY_SEPARATOR === '/' && !@is_readable('/dev/urandom')) { + $open_basedir = ini_get('open_basedir'); + if ($open_basedir) { + $open_basedir_paths = explode(PATH_SEPARATOR, $open_basedir); + $open_basedir_blocks_urandom = !array_intersect(array('/dev', '/dev/', '/dev/urandom'), $open_basedir_paths); + } + } + $args = array( + ':drupal-php' => 'https://www.drupal.org/docs/7/system-requirements/php#csprng', + '%exception_message' => $e->getMessage(), + ); + if ($open_basedir_blocks_urandom) { + $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. The most likely cause is that open_basedir restrictions are in effect and /dev/urandom is not on the whitelist. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args); + } + else { + $requirements['php_random_bytes']['description'] = t('Drupal is unable to generate highly randomized numbers, which means certain security features like password reset URLs are not as secure as they should be. Instead, only a slow, less-secure fallback generator is available. See the <a href=":drupal-php">system requirements</a> page for more information. %exception_message', $args); + } + $requirements['php_random_bytes']['value'] = t('Less secure'); + $requirements['php_random_bytes']['severity'] = REQUIREMENT_ERROR; + } + } + if ($phase == 'install' || $phase == 'update') { // Test for PDO (database). $requirements['database_extensions'] = array( diff --git a/core/tests/Drupal/Tests/Component/Utility/CryptRandomFallbackTest.php b/core/tests/Drupal/Tests/Component/Utility/CryptRandomFallbackTest.php new file mode 100644 index 0000000000000000000000000000000000000000..de00da6a4b6ff158e26a192534c3789695640e0c --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Utility/CryptRandomFallbackTest.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\Tests\Component\Utility; + +use Drupal\Tests\UnitTestCase; +use Drupal\Component\Utility\Crypt; + +/** + * Tests random byte generation fallback exception situations. + * + * @group Utility + * + * @runTestsInSeparateProcesses + * + * @coversDefaultClass \Drupal\Component\Utility\Crypt + */ +class CryptRandomFallbackTest extends UnitTestCase { + + static protected $functionCalled = 0; + + /** + * Allows the test to confirm that the namespaced random_bytes() was called. + */ + public static function functionCalled() { + static::$functionCalled++; + } + + /** + * Tests random byte generation using the fallback generator. + * + * If the call to random_bytes() throws an exception, Crypt::random_bytes() + * should still return a useful string of random bytes. + * + * @covers ::randomBytes + * + * @see \Drupal\Tests\Component\Utility\CryptTest::testRandomBytes() + */ + public function testRandomBytesFallback() { + // This loop is a copy of + // \Drupal\Tests\Component\Utility\CryptTest::testRandomBytes(). + for ($i = 0; $i < 10; $i++) { + $count = rand(10, 10000); + // Check that different values are being generated. + $this->assertNotEquals(Crypt::randomBytes($count), Crypt::randomBytes($count)); + // Check the length. + $this->assertEquals($count, strlen(Crypt::randomBytes($count))); + } + $this->assertEquals(30, static::$functionCalled, 'The namespaced function was called the expected number of times.'); + } + +} + +namespace Drupal\Component\Utility; + +use \Drupal\Tests\Component\Utility\CryptRandomFallbackTest; + +/** + * Defines a function in same namespace as Drupal\Component\Utility\Crypt. + * + * Forces throwing an exception in this test environment because the function + * in the namespace is used in preference to the global function. + * + * @param int $count + * Matches the global function definition. + * + * @throws \Exception + */ +function random_bytes($count) { + CryptRandomFallbackTest::functionCalled(); + throw new \Exception($count); +} diff --git a/core/tests/Drupal/Tests/Component/Utility/CryptTest.php b/core/tests/Drupal/Tests/Component/Utility/CryptTest.php index ff1e6ccfbfb072fc54db5ccdbaf0f421c6715431..42d6015158a4642870c92ffd15dd9a83abdfc3a1 100644 --- a/core/tests/Drupal/Tests/Component/Utility/CryptTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/CryptTest.php @@ -18,6 +18,8 @@ class CryptTest extends UnitTestCase { * Tests random byte generation. * * @covers ::randomBytes + * + * @see \Drupal\Tests\Component\Utility\CryptRandomFallbackTest::testRandomBytesFallback */ public function testRandomBytes() { for ($i = 1; $i < 10; $i++) {