Browse Source

Fix #18817: Use `paragonie/random_compat` for random bytes and int generation

tags/2.0.43
Alexander Makarov 3 years ago committed by GitHub
parent
commit
13f27e4d92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      composer.json
  2. 112
      composer.lock
  3. 1
      framework/CHANGELOG.md
  4. 89
      framework/base/Security.php
  5. 3
      framework/caching/DbCache.php
  6. 2
      framework/caching/FileCache.php
  7. 17
      framework/captcha/CaptchaAction.php
  8. 3
      framework/composer.json
  9. 2
      framework/mail/BaseMailer.php
  10. 191
      tests/framework/base/SecurityTest.php
  11. 4
      tests/framework/validators/FileValidatorTest.php
  12. 11
      tests/framework/web/UploadedFileTest.php

3
composer.json

@ -78,7 +78,8 @@
"bower-asset/jquery": "3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/inputmask": "~3.2.2 | ~3.3.5",
"bower-asset/punycode": "1.3.*",
"bower-asset/yii2-pjax": "~2.0.1"
"bower-asset/yii2-pjax": "~2.0.1",
"paragonie/random_compat": ">=1"
},
"require-dev": {
"cweagans/composer-patches": "^1.7",

112
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7e41c6fc0175fd049e0e52c4e8b25e5c",
"content-hash": "8f9a4d7e645592f806605d32d676f54e",
"packages": [
{
"name": "bower-asset/inputmask",
@ -200,6 +200,60 @@
"time": "2020-06-29T00:56:53+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v2.0.20",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "0f1f60250fccffeaf5dda91eea1c018aed1adc2a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/0f1f60250fccffeaf5dda91eea1c018aed1adc2a",
"reference": "0f1f60250fccffeaf5dda91eea1c018aed1adc2a",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2021-04-17T09:33:01+00:00"
},
{
"name": "yiisoft/yii2-composer",
"version": "2.0.10",
"source": {
@ -892,60 +946,6 @@
"time": "2015-09-13T19:01:00+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v2.0.20",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "0f1f60250fccffeaf5dda91eea1c018aed1adc2a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/0f1f60250fccffeaf5dda91eea1c018aed1adc2a",
"reference": "0f1f60250fccffeaf5dda91eea1c018aed1adc2a",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2021-04-17T09:33:01+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "2.0.5",
"source": {
@ -2919,5 +2919,5 @@
"platform-overrides": {
"php": "5.4"
},
"plugin-api-version": "2.0.0"
"plugin-api-version": "2.1.0"
}

1
framework/CHANGELOG.md

@ -4,6 +4,7 @@ Yii Framework 2 Change Log
2.0.43 under development
------------------------
- Enh #18817: Use `paragonie/random_compat` for random bytes and int generation (samdark)
- Bug #14663: Do not convert int to string if database type of a column is numeric (egorrishe)
- Bug #18650: Refactor `framework/assets/yii.activeForm.js` arrow function into traditional function for IE11 compatibility (marcovtwout)
- Bug #18749: Fix `yii\web\ErrorHandler::encodeHtml()` to support strings with invalid UTF symbols (vjik)

89
framework/base/Security.php

@ -117,14 +117,6 @@ class Security extends Component
}
/**
* @return bool if operating system is Windows
*/
private function isWindows()
{
return DIRECTORY_SEPARATOR !== '/';
}
/**
* Encrypts data using a password.
* Derives keys for encryption and authentication from the password using PBKDF2 and a random salt,
* which is deliberately slow to protect against dictionary attacks. Use [[encryptByKey()]] to
@ -471,8 +463,6 @@ class Security extends Component
return false;
}
private $_randomFile;
/**
* Generates specified number of random bytes.
* Note that output may not be ASCII.
@ -493,84 +483,7 @@ class Security extends Component
throw new InvalidArgumentException('First parameter ($length) must be greater than 0');
}
// always use random_bytes() if it is available
if (function_exists('random_bytes')) {
return random_bytes($length);
}
// The recent LibreSSL RNGs are faster and likely better than /dev/urandom.
// Since 5.4.0, openssl_random_pseudo_bytes() reads from CryptGenRandom on Windows instead
// of using OpenSSL library. LibreSSL is OK everywhere but don't use OpenSSL on non-Windows.
if (function_exists('openssl_random_pseudo_bytes')
&& ($this->shouldUseLibreSSL() || $this->isWindows())
) {
$key = openssl_random_pseudo_bytes($length, $cryptoStrong);
if ($cryptoStrong === false) {
throw new Exception(
'openssl_random_pseudo_bytes() set $crypto_strong false. Your PHP setup is insecure.'
);
}
if ($key !== false && StringHelper::byteLength($key) === $length) {
return $key;
}
}
// mcrypt_create_iv() does not use libmcrypt. Since PHP 5.3.7 it directly reads
// CryptGenRandom on Windows. Elsewhere it directly reads /dev/urandom.
if (function_exists('mcrypt_create_iv')) {
$key = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
if (StringHelper::byteLength($key) === $length) {
return $key;
}
}
// If not on Windows, try to open a random device.
if ($this->_randomFile === null && !$this->isWindows()) {
// urandom is a symlink to random on FreeBSD.
$device = PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom';
// Check random device for special character device protection mode. Use lstat()
// instead of stat() in case an attacker arranges a symlink to a fake device.
$lstat = @lstat($device);
if ($lstat !== false && ($lstat['mode'] & 0170000) === 020000) {
$this->_randomFile = fopen($device, 'rb') ?: null;
if (is_resource($this->_randomFile)) {
// Reduce PHP stream buffer from default 8192 bytes to optimize data
// transfer from the random device for smaller values of $length.
// This also helps to keep future randoms out of user memory space.
$bufferSize = 8;
if (function_exists('stream_set_read_buffer')) {
stream_set_read_buffer($this->_randomFile, $bufferSize);
}
// stream_set_read_buffer() isn't implemented on HHVM
if (function_exists('stream_set_chunk_size')) {
stream_set_chunk_size($this->_randomFile, $bufferSize);
}
}
}
}
if (is_resource($this->_randomFile)) {
$buffer = '';
$stillNeed = $length;
while ($stillNeed > 0) {
$someBytes = fread($this->_randomFile, $stillNeed);
if ($someBytes === false) {
break;
}
$buffer .= $someBytes;
$stillNeed -= StringHelper::byteLength($someBytes);
if ($stillNeed === 0) {
// Leaving file pointer open in order to make next generation faster by reusing it.
return $buffer;
}
}
fclose($this->_randomFile);
$this->_randomFile = null;
}
throw new Exception('Unable to generate a random key');
return random_bytes($length);
}
/**

3
framework/caching/DbCache.php

@ -276,7 +276,8 @@ class DbCache extends Cache
*/
public function gc($force = false)
{
if ($force || mt_rand(0, 1000000) < $this->gcProbability) {
if ($force || random_int(0, 1000000) < $this->gcProbability) {
$this->db->createCommand()
->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time())
->execute();

2
framework/caching/FileCache.php

@ -245,7 +245,7 @@ class FileCache extends Cache
*/
public function gc($force = false, $expiredOnly = true)
{
if ($force || mt_rand(0, 1000000) < $this->gcProbability) {
if ($force || random_int(0, 1000000) < $this->gcProbability) {
$this->gcRecursive($this->cachePath, $expiredOnly);
}
}

17
framework/captcha/CaptchaAction.php

@ -214,16 +214,17 @@ class CaptchaAction extends Action
if ($this->maxLength > 20) {
$this->maxLength = 20;
}
$length = mt_rand($this->minLength, $this->maxLength);
$length = random_int($this->minLength, $this->maxLength);
$letters = 'bcdfghjklmnpqrstvwxyz';
$vowels = 'aeiou';
$code = '';
for ($i = 0; $i < $length; ++$i) {
if ($i % 2 && mt_rand(0, 10) > 2 || !($i % 2) && mt_rand(0, 10) > 9) {
$code .= $vowels[mt_rand(0, 4)];
if ($i % 2 && random_int(0, 10) > 2 || !($i % 2) && random_int(0, 10) > 9) {
$code .= $vowels[random_int(0, 4)];
} else {
$code .= $letters[mt_rand(0, 20)];
$code .= $letters[random_int(0, 20)];
}
}
@ -298,8 +299,8 @@ class CaptchaAction extends Action
$x = 10;
$y = round($this->height * 27 / 40);
for ($i = 0; $i < $length; ++$i) {
$fontSize = (int) (mt_rand(26, 32) * $scale * 0.8);
$angle = mt_rand(-10, 10);
$fontSize = (int) (random_int(26, 32) * $scale * 0.8);
$angle = random_int(-10, 10);
$letter = $code[$i];
$box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter);
$x = $box[2] + $this->offset;
@ -341,9 +342,9 @@ class CaptchaAction extends Action
for ($i = 0; $i < $length; ++$i) {
$draw = new \ImagickDraw();
$draw->setFont($this->fontFile);
$draw->setFontSize((int) (mt_rand(26, 32) * $scale * 0.8));
$draw->setFontSize((int) (random_int(26, 32) * $scale * 0.8));
$draw->setFillColor($foreColor);
$image->annotateImage($draw, $x, $y, mt_rand(-10, 10), $code[$i]);
$image->annotateImage($draw, $x, $y, random_int(-10, 10), $code[$i]);
$fontMetrics = $image->queryFontMetrics($draw, $code[$i]);
$x += (int) $fontMetrics['textWidth'] + $this->offset;
}

3
framework/composer.json

@ -73,7 +73,8 @@
"bower-asset/jquery": "3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/inputmask": "~3.2.2 | ~3.3.5",
"bower-asset/punycode": "1.3.*",
"bower-asset/yii2-pjax": "~2.0.1"
"bower-asset/yii2-pjax": "~2.0.1",
"paragonie/random_compat": ">=1"
},
"autoload": {
"psr-4": {"yii\\": ""}

2
framework/mail/BaseMailer.php

@ -343,7 +343,7 @@ abstract class BaseMailer extends Component implements MailerInterface, ViewCont
{
$time = microtime(true);
return date('Ymd-His-', $time) . sprintf('%04d', (int) (($time - (int) $time) * 10000)) . '-' . sprintf('%04d', mt_rand(0, 10000)) . '.eml';
return date('Ymd-His-', $time) . sprintf('%04d', (int) (($time - (int) $time) * 10000)) . '-' . sprintf('%04d', random_int(0, 10000)) . '.eml';
}
/**

191
tests/framework/base/SecurityTest.php

@ -5,53 +5,7 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\base {
/**
* emulate availability of functions, to test different branches of Security class
* where different execution paths are chosen based on calling function_exists.
*
* This function overrides function_exists from the root namespace in yii\base.
* @param string $name
*/
function function_exists($name)
{
if (isset(\yiiunit\framework\base\SecurityTest::$functions[$name])) {
return \yiiunit\framework\base\SecurityTest::$functions[$name];
}
return \function_exists($name);
}
/**
* Emulate chunked reading of fread(), to test different branches of Security class
* where different execution paths are chosen based on the return value of fopen/fread.
*
* This function overrides fopen and fread from the root namespace in yii\base.
* @param string $filename
* @param mixed $mode
*/
function fopen($filename, $mode)
{
if (\yiiunit\framework\base\SecurityTest::$fopen !== null) {
return \yiiunit\framework\base\SecurityTest::$fopen;
}
return \fopen($filename, $mode);
}
function fread($handle, $length)
{
if (\yiiunit\framework\base\SecurityTest::$fread !== null) {
return \yiiunit\framework\base\SecurityTest::$fread;
}
if (\yiiunit\framework\base\SecurityTest::$fopen !== null) {
return $length < 8 ? \str_repeat('s', $length) : 'test1234';
}
return \fread($handle, $length);
}
} // closing namespace yii\base;
namespace yiiunit\framework\base {
namespace yiiunit\framework\base;
use yii\base\Security;
use yiiunit\TestCase;
@ -64,43 +18,17 @@ class SecurityTest extends TestCase
const CRYPT_VECTORS = 'old';
/**
* @var array set of functions for which a fake return value for `function_exists()` is provided.
*/
public static $functions = [];
/**
* @var resource|false|null fake return value for fopen() in \yii\base namespace. Normal behavior if this is null.
*/
public static $fopen;
public static $fread;
/**
* @var ExposedSecurity
*/
protected $security;
protected function setUp()
{
static::$functions = [];
static::$fopen = null;
static::$fread = null;
parent::setUp();
$this->security = new ExposedSecurity();
$this->security->derivationIterations = 1000; // speed up test running
}
protected function tearDown()
{
static::$functions = [];
static::$fopen = null;
static::$fread = null;
parent::tearDown();
}
private function isWindows()
{
return DIRECTORY_SEPARATOR !== '/';
}
// Tests :
public function testHashData()
@ -893,76 +821,8 @@ TEXT;
$key1 = $this->security->generateRandomKey($input);
}
/**
* Test the case where opening /dev/urandom fails.
*/
public function testRandomKeyNoOptions()
{
static::$functions = ['random_bytes' => false, 'openssl_random_pseudo_bytes' => false, 'mcrypt_create_iv' => false];
static::$fopen = false;
$this->expectException('yii\base\Exception');
$this->expectExceptionMessage('Unable to generate a random key');
$this->security->generateRandomKey(42);
}
/**
* Test the case where reading from /dev/urandom fails.
*/
public function testRandomKeyFreadFailure()
{
static::$functions = ['random_bytes' => false, 'openssl_random_pseudo_bytes' => false, 'mcrypt_create_iv' => false];
static::$fread = false;
$this->expectException('yii\base\Exception');
$this->expectExceptionMessage('Unable to generate a random key');
$this->security->generateRandomKey(42);
}
/**
* returns a set of different combinations of functions available.
*/
public function randomKeyVariants()
public function testGenerateRandomKey()
{
return [
[['random_bytes' => true, 'openssl_random_pseudo_bytes' => true, 'mcrypt_create_iv' => true]],
[['random_bytes' => true, 'openssl_random_pseudo_bytes' => true, 'mcrypt_create_iv' => false]],
[['random_bytes' => true, 'openssl_random_pseudo_bytes' => false, 'mcrypt_create_iv' => true]],
[['random_bytes' => true, 'openssl_random_pseudo_bytes' => false, 'mcrypt_create_iv' => false]],
[['random_bytes' => false, 'openssl_random_pseudo_bytes' => true, 'mcrypt_create_iv' => true]],
[['random_bytes' => false, 'openssl_random_pseudo_bytes' => true, 'mcrypt_create_iv' => false]],
[['random_bytes' => false, 'openssl_random_pseudo_bytes' => false, 'mcrypt_create_iv' => true]],
[['random_bytes' => false, 'openssl_random_pseudo_bytes' => false, 'mcrypt_create_iv' => false]],
];
}
/**
* @dataProvider randomKeyVariants
* @param array $functions
*/
public function testGenerateRandomKey($functions)
{
foreach ($functions as $fun => $available) {
if ($available && !\function_exists($fun)) {
$this->markTestSkipped("Can not test generateRandomKey() branch that includes $fun, because it is not available on your system.");
}
}
// there is no /dev/urandom on windows so we expect this to fail
if ($this->isWindows() && $functions['random_bytes'] === false && $functions['openssl_random_pseudo_bytes'] === false && $functions['mcrypt_create_iv'] === false) {
$this->expectException('yii\base\Exception');
$this->expectExceptionMessage('Unable to generate a random key');
}
// Function mcrypt_create_iv() is deprecated since PHP 7.1
if (version_compare(PHP_VERSION, '7.1.0alpha', '>=') && $functions['random_bytes'] === false && $functions['mcrypt_create_iv'] === true) {
if ($functions['openssl_random_pseudo_bytes'] === false) {
$this->markTestSkipped('Function mcrypt_create_iv() is deprecated as of PHP 7.1');
} elseif (!$this->security->shouldUseLibreSSL() && !$this->isWindows()) {
$this->markTestSkipped('Function openssl_random_pseudo_bytes need LibreSSL version >=2.1.5 or Windows system on server');
}
}
static::$functions = $functions;
// test various string lengths
for ($length = 1; $length < 64; $length++) {
$key1 = $this->security->generateRandomKey($length);
@ -985,16 +845,6 @@ TEXT;
$this->assertInternalType('string', $key2);
$this->assertEquals($length, strlen($key2));
$this->assertNotEquals($key1, $key2);
// force /dev/urandom reading loop to deal with chunked data
// the above test may have read everything in one run.
// not sure if this can happen in real life but if it does
// we should be prepared
static::$fopen = fopen('php://memory', 'rwb');
$length = 1024 * 1024;
$key1 = $this->security->generateRandomKey($length);
$this->assertInternalType('string', $key1);
$this->assertEquals($length, strlen($key1));
}
protected function randTime(Security $security, $count, $length, $message)
@ -1010,42 +860,6 @@ TEXT;
fwrite(STDERR, "$message: $count x $length B = $nbytes B in $milisec ms => $rate MB/s\n");
}
public function testGenerateRandomKeySpeed()
{
self::markTestSkipped('Comment markTestSkipped in testGenerateRandomKeySpeed() in order to get RNG benchmark.');
$tests = [
"function_exists('random_bytes')",
"defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : null",
'PHP_VERSION_ID',
'PHP_OS',
"function_exists('mcrypt_create_iv') ? bin2hex(mcrypt_create_iv(4, MCRYPT_DEV_URANDOM)) : null",
'DIRECTORY_SEPARATOR',
"ini_get('open_basedir')",
];
if ($this->isWindows()) {
$tests[] = "sprintf('%o', lstat(PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom')['mode'] & 0170000)";
$tests[] = "bin2hex(file_get_contents(PHP_OS === 'FreeBSD' ? '/dev/random' : '/dev/urandom', false, null, 0, 8))";
}
foreach ($tests as $i => $test) {
$result = eval('return ' . $test . ';');
fwrite(STDERR, sprintf("%2d %s ==> %s\n", $i + 1, $test, var_export($result, true)));
}
foreach ([16, 2000, 262144] as $block) {
$security = new Security();
foreach (range(1, 10) as $nth) {
$this->randTime($security, 1, $block, "Call $nth");
}
unset($security);
}
$security = new Security();
$this->randTime($security, 10000, 16, 'Rate test');
$security = new Security();
$this->randTime($security, 10000, 5000, 'Rate test');
}
public function testGenerateRandomString()
{
$length = 21;
@ -1305,4 +1119,3 @@ TEXT;
];
}
}
} // closing namespace yiiunit\framework\base;

4
tests/framework/validators/FileValidatorTest.php

@ -324,7 +324,7 @@ class FileValidatorTest extends TestCase
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randomString = '';
for ($i = 0; $i < $len; $i++) {
$randomString .= $characters[rand(0, strlen($characters) - 1)];
$randomString .= $characters[random_int(0, strlen($characters) - 1)];
}
return $randomString;
@ -340,7 +340,7 @@ class FileValidatorTest extends TestCase
if (is_readable($tempName)) {
$size = filesize($tempName);
} else {
$size = isset($param['size']) ? $param['size'] : rand(
$size = isset($param['size']) ? $param['size'] : random_int(
1,
$this->sizeToBytes(ini_get('upload_max_filesize'))
);

11
tests/framework/web/UploadedFileTest.php

@ -7,6 +7,7 @@
namespace yiiunit\framework\web;
use Yii;
use yii\web\UploadedFile;
use yiiunit\framework\web\mocks\UploadedFileMock;
use yiiunit\framework\web\stubs\ModelStub;
@ -28,10 +29,10 @@ class UploadedFileTest extends TestCase
private function generateFakeFileData()
{
return [
'name' => md5(mt_rand()),
'tmp_name' => md5(mt_rand()),
'name' => md5(random_int(0, PHP_INT_MAX)),
'tmp_name' => md5(random_int(0, PHP_INT_MAX)),
'type' => 'image/jpeg',
'size' => mt_rand(1000, 10000),
'size' => random_int(1000, 10000),
'error' => '0',
];
}
@ -39,10 +40,10 @@ class UploadedFileTest extends TestCase
private function generateTempFileData()
{
return [
'name' => md5(mt_rand()),
'name' => md5(random_int(0, PHP_INT_MAX)),
'tmp_name' => tempnam(sys_get_temp_dir(), ''),
'type' => 'image/jpeg',
'size' => mt_rand(1000, 10000),
'size' => random_int(1000, 10000),
'error' => '0',
];
}

Loading…
Cancel
Save