CVE-2021-3692 (PHP, published 2021-08-10T17:15Z)
Description: value: yii2 is vulnerable to Use of Predictable Algorithm in Random Number Generator
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: CWE-330 \u2014 Use of Insufficiently Random Values
The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.
Repository: https://github.com/yiisoft/yii2
Files changed:
composer.json
composer.lock
framework/CHANGELOG.md
framework/base/Security.php
framework/caching/DbCache.php
framework/caching/FileCache.php
framework/captcha/CaptchaAction.php
framework/composer.json
framework/mail/BaseMailer.php
tests/framework/base/SecurityTest.php
tests/framework/validators/FileValidatorTest.php
tests/framework/web/UploadedFileTest.php
Commit message: Fix #18817: Use `paragonie/random_compat` for random bytes and int generation



Vulnerable code:
        "bower-asset/yii2-pjax": "~2.0.1"
    "content-hash": "7e41c6fc0175fd049e0e52c4e8b25e5c",
        {
            "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"
        },
    "plugin-api-version": "2.0.0"
    /**
     * @return bool if operating system is Windows
     */
    private function isWindows()
    {
        return DIRECTORY_SEPARATOR !== '/';
    }

    private $_randomFile;

        // 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');
        if ($force || mt_rand(0, 1000000) < $this->gcProbability) {
        if ($force || mt_rand(0, 1000000) < $this->gcProbability) {
        $length = mt_rand($this->minLength, $this->maxLength);
            if ($i % 2 && mt_rand(0, 10) > 2 || !($i % 2) && mt_rand(0, 10) > 9) {
                $code .= $vowels[mt_rand(0, 4)];
                $code .= $letters[mt_rand(0, 20)];
            $fontSize = (int) (mt_rand(26, 32) * $scale * 0.8);
            $angle = mt_rand(-10, 10);
            $draw->setFontSize((int) (mt_rand(26, 32) * $scale * 0.8));
            $image->annotateImage($draw, $x, $y, mt_rand(-10, 10), $code[$i]);
        "bower-asset/yii2-pjax": "~2.0.1"
        return date('Ymd-His-', $time) . sprintf('%04d', (int) (($time - (int) $time) * 10000)) . '-' . sprintf('%04d', mt_rand(0, 10000)) . '.eml';
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 {
    /**
     * @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;

        static::$functions = [];
        static::$fopen = null;
        static::$fread = null;
    protected function tearDown()
    {
        static::$functions = [];
        static::$fopen = null;
        static::$fread = null;
        parent::tearDown();
    }

    private function isWindows()
    {
        return DIRECTORY_SEPARATOR !== '/';
    }

    /**
     * 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()
        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;


        // 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));
    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');
    }

} // closing namespace yiiunit\framework\base;
                $randomString .= $characters[rand(0, strlen($characters) - 1)];
                $size = isset($param['size']) ? $param['size'] : rand(
            'name' => md5(mt_rand()),
            'tmp_name' => md5(mt_rand()),
            'size' => mt_rand(1000, 10000),
            'name' => md5(mt_rand()),
            'size' => mt_rand(1000, 10000),

Fixed code:
        "bower-asset/yii2-pjax": "~2.0.1",
        "paragonie/random_compat": ">=1"
    "content-hash": "8f9a4d7e645592f806605d32d676f54e",
        {
            "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"
        },
    "plugin-api-version": "2.1.0"
- Enh #18817: Use `paragonie/random_compat` for random bytes and int generation (samdark)
        return random_bytes($length);

        if ($force || random_int(0, 1000000) < $this->gcProbability) {
        if ($force || random_int(0, 1000000) < $this->gcProbability) {

        $length = random_int($this->minLength, $this->maxLength);
            if ($i % 2 && random_int(0, 10) > 2 || !($i % 2) && random_int(0, 10) > 9) {
                $code .= $vowels[random_int(0, 4)];
                $code .= $letters[random_int(0, 20)];
            $fontSize = (int) (random_int(26, 32) * $scale * 0.8);
            $angle = random_int(-10, 10);
            $draw->setFontSize((int) (random_int(26, 32) * $scale * 0.8));
            $image->annotateImage($draw, $x, $y, random_int(-10, 10), $code[$i]);
        "bower-asset/yii2-pjax": "~2.0.1",
        "paragonie/random_compat": ">=1"
        return date('Ymd-His-', $time) . sprintf('%04d', (int) (($time - (int) $time) * 10000)) . '-' . sprintf('%04d', random_int(0, 10000)) . '.eml';
namespace yiiunit\framework\base;
    public function testGenerateRandomKey()
                $randomString .= $characters[random_int(0, strlen($characters) - 1)];
                $size = isset($param['size']) ? $param['size'] : random_int(
use Yii;
            'name' => md5(random_int(0, PHP_INT_MAX)),
            'tmp_name' => md5(random_int(0, PHP_INT_MAX)),
            'size' => random_int(1000, 10000),
            'name' => md5(random_int(0, PHP_INT_MAX)),
            'size' => random_int(1000, 10000),