Package: release.debian.org Severity: normal Tags: jessie User: release.debian....@packages.debian.org Usertags: pu
Hi, As agreed with the security team, this update aims to fix a security issue in zendframework (request for Wheezy follows) via pu. Please find attached the debdiff, as well as the actual patch with some formating noise removed. * Backport security fix from 1.12.17: - ZF2015-09: Fixed entropy issue in word CAPTCHA http://framework.zend.com/security/advisory/ZF2015-09 Thanks in advance for considering. Regards David
diff --git a/debian/changelog b/debian/changelog index 915004f..9977720 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +zendframework (1.12.9+dfsg-2+deb8u5) jessie; urgency=medium + + * Backport security fix from 1.12.17 + - ZF2015-09: Fixed entropy issue in word CAPTCHA + http://framework.zend.com/security/advisory/ZF2015-09 + + -- David Prévot <taf...@debian.org> Tue, 24 Nov 2015 18:21:26 -0400 + zendframework (1.12.9+dfsg-2+deb8u4) jessie-security; urgency=high * Backport security fixes from 1.12.16: diff --git a/debian/patches/0008-ZF2015-09-Fixed-entropy-issue-in-word-CAPTCHA.patch b/debian/patches/0008-ZF2015-09-Fixed-entropy-issue-in-word-CAPTCHA.patch new file mode 100644 index 0000000..412b779 --- /dev/null +++ b/debian/patches/0008-ZF2015-09-Fixed-entropy-issue-in-word-CAPTCHA.patch @@ -0,0 +1,347 @@ +From: Enrico Zimuel <e.zim...@gmail.com> +Date: Mon, 9 Nov 2015 17:26:45 +0100 +Subject: ZF2015-09: Fixed entropy issue in word CAPTCHA + +This patch fixes a potential entropy fixation vector with `Zend_Captcha_Word`. +Prior to the fix, when selecting letters for the CAPTCHA, `array_rand()` was +used, which does not use sufficient entropy during randomization. The patch +backports randomization routines from ZF2 in order to provide a more +cryptographically secure RNG. + +Origin: upstream, https://github.com/zendframework/zf1/commit/4a41392f89bf510a8ab801eacb117fe7ea25b575 +--- + library/Zend/Captcha/Word.php | 29 +++++++----- + library/Zend/Crypt/Math.php | 100 +++++++++++++++++++++++++++++++++++++++--- + tests/Zend/Crypt/MathTest.php | 75 +++++++++++++++++++++++++++++-- + 3 files changed, 183 insertions(+), 21 deletions(-) + +diff --git a/library/Zend/Captcha/Word.php b/library/Zend/Captcha/Word.php +index 1f0e0fc..ba39580 100644 +--- a/library/Zend/Captcha/Word.php ++++ b/library/Zend/Captcha/Word.php +@@ -22,6 +22,9 @@ + /** @see Zend_Captcha_Base */ + require_once 'Zend/Captcha/Base.php'; + ++/** @see Zend_Crypt_Math */ ++require_once 'Zend/Crypt/Math.php'; ++ + /** + * Word-based captcha adapter + * +@@ -39,10 +42,10 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + /**#@+ + * @var array Character sets + */ +- static $V = array("a", "e", "i", "o", "u", "y"); +- static $VN = array("a", "e", "i", "o", "u", "y","2","3","4","5","6","7","8","9"); +- static $C = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z"); +- static $CN = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z","2","3","4","5","6","7","8","9"); ++ static public $V = array("a", "e", "i", "o", "u", "y"); ++ static public $VN = array("a", "e", "i", "o", "u", "y","2","3","4","5","6","7","8","9"); ++ static public $C = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z"); ++ static public $CN = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z","2","3","4","5","6","7","8","9"); + /**#@-*/ + + /** +@@ -175,7 +178,7 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + * + * @return string + */ +- public function getId () ++ public function getId() + { + if (null === $this->_id) { + $this->_setId($this->_generateRandomId()); +@@ -189,7 +192,7 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + * @param string $id + * @return Zend_Captcha_Word + */ +- protected function _setId ($id) ++ protected function _setId($id) + { + $this->_id = $id; + return $this; +@@ -250,7 +253,7 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + $this->_useNumbers = $_useNumbers; + return $this; + } +- ++ + /** + * Get session object + * +@@ -280,7 +283,7 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + public function setSession(Zend_Session_Namespace $session) + { + $this->_session = $session; +- if($session) { ++ if ($session) { + $this->_keepSession = true; + } + return $this; +@@ -326,10 +329,12 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + $vowels = $this->_useNumbers ? self::$VN : self::$V; + $consonants = $this->_useNumbers ? self::$CN : self::$C; + ++ $totIndexCon = count($consonants) - 1; ++ $totIndexVow = count($vowels) - 1; + for ($i=0; $i < $wordLen; $i = $i + 2) { + // generate word with mix of vowels and consonants +- $consonant = $consonants[array_rand($consonants)]; +- $vowel = $vowels[array_rand($vowels)]; ++ $consonant = $consonants[Zend_Crypt_Math::randInteger(0, $totIndexCon, true)]; ++ $vowel = $vowels[Zend_Crypt_Math::randInteger(0, $totIndexVow, true)]; + $word .= $consonant . $vowel; + } + +@@ -347,7 +352,7 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + */ + public function generate() + { +- if(!$this->_keepSession) { ++ if (!$this->_keepSession) { + $this->_session = null; + } + $id = $this->_generateRandomId(); +@@ -359,7 +364,7 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base + + protected function _generateRandomId() + { +- return md5(mt_rand(0, 1000) . microtime(true)); ++ return md5(Zend_Crypt_Math::randBytes(32)); + } + + /** +diff --git a/library/Zend/Crypt/Math.php b/library/Zend/Crypt/Math.php +index 40395f5..8882259 100644 +--- a/library/Zend/Crypt/Math.php ++++ b/library/Zend/Crypt/Math.php +@@ -57,21 +57,109 @@ class Zend_Crypt_Math extends Zend_Crypt_Math_BigInteger + } + $rand = ''; + $i2 = strlen($maximum) - 1; +- for ($i = 1;$i < $i2;$i++) { +- $rand .= mt_rand(0,9); ++ for ($i = 1; $i < $i2; $i++) { ++ $rand .= mt_rand(0, 9); + } +- $rand .= mt_rand(0,9); ++ $rand .= mt_rand(0, 9); + return $rand; + } + + /** ++ * Return a random strings of $length bytes ++ * ++ * @param integer $length ++ * @param boolean $strong ++ * @return string ++ */ ++ public static function randBytes($length, $strong = false) ++ { ++ $length = (int) $length; ++ if ($length <= 0) { ++ return false; ++ } ++ if (function_exists('openssl_random_pseudo_bytes')) { ++ $bytes = openssl_random_pseudo_bytes($length, $usable); ++ if ($strong === $usable) { ++ return $bytes; ++ } ++ } ++ if (function_exists('mcrypt_create_iv')) { ++ $bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); ++ if ($bytes !== false && strlen($bytes) === $length) { ++ return $bytes; ++ } ++ } ++ if (file_exists('/dev/urandom') && is_readable('/dev/urandom')) { ++ $frandom = fopen('/dev/urandom', 'r'); ++ if ($frandom !== false) { ++ return fread($frandom, $length); ++ } ++ } ++ if (true === $strong) { ++ require_once 'Zend/Crypt/Exception.php'; ++ throw new Zend_Crypt_Exception( ++ 'This PHP environment doesn\'t support secure random number generation. ' . ++ 'Please consider installing the OpenSSL and/or Mcrypt extensions' ++ ); ++ } ++ $rand = ''; ++ for ($i = 0; $i < $length; $i++) { ++ $rand .= chr(mt_rand(0, 255)); ++ } ++ return $rand; ++ } ++ ++ /** ++ * Return a random integer between $min and $max ++ * ++ * @param integer $min ++ * @param integer $max ++ * @param boolean $strong ++ * @return integer ++ */ ++ public static function randInteger($min, $max, $strong = false) ++ { ++ if ($min > $max) { ++ require_once 'Zend/Crypt/Exception.php'; ++ throw new Zend_Crypt_Exception( ++ 'The min parameter must be lower than max parameter' ++ ); ++ } ++ $range = $max - $min; ++ if ($range == 0) { ++ return $max; ++ } elseif ($range > PHP_INT_MAX || is_float($range)) { ++ require_once 'Zend/Crypt/Exception.php'; ++ throw new Zend_Crypt_Exception( ++ 'The supplied range is too great to generate' ++ ); ++ } ++ // calculate number of bits required to store range on this machine ++ $r = $range; ++ $bits = 0; ++ while ($r) { ++ $bits++; ++ $r >>= 1; ++ } ++ $bits = (int) max($bits, 1); ++ $bytes = (int) max(ceil($bits / 8), 1); ++ $filter = (int) ((1 << $bits) - 1); ++ do { ++ $rnd = hexdec(bin2hex(self::randBytes($bytes, $strong))); ++ $rnd &= $filter; ++ } while ($rnd > $range); ++ return ($min + $rnd); ++ } ++ ++ /** + * Get the big endian two's complement of a given big integer in + * binary notation + * + * @param string $long + * @return string + */ +- public function btwoc($long) { ++ public function btwoc($long) ++ { + if (ord($long[0]) > 127) { + return "\x00" . $long; + } +@@ -84,7 +172,8 @@ class Zend_Crypt_Math extends Zend_Crypt_Math_BigInteger + * @param string $binary + * @return string + */ +- public function fromBinary($binary) { ++ public function fromBinary($binary) ++ { + return $this->_math->binaryToInteger($binary); + } + +@@ -98,5 +187,4 @@ class Zend_Crypt_Math extends Zend_Crypt_Math_BigInteger + { + return $this->_math->integerToBinary($integer); + } +- + } +diff --git a/tests/Zend/Crypt/MathTest.php b/tests/Zend/Crypt/MathTest.php +index eeb9325..79bd02e 100644 +--- a/tests/Zend/Crypt/MathTest.php ++++ b/tests/Zend/Crypt/MathTest.php +@@ -21,7 +21,7 @@ + */ + + require_once 'Zend/Crypt/Math.php'; +- ++require_once 'Zend/Crypt/Exception.php'; + + /** + * @category Zend +@@ -36,8 +36,7 @@ class Zend_Crypt_MathTest extends PHPUnit_Framework_TestCase + + public function testRand() + { +- if (!extension_loaded('bcmath')) +- { ++ if (!extension_loaded('bcmath')) { + $this->markTestSkipped('Extension bcmath not loaded'); + } + +@@ -59,4 +58,74 @@ class Zend_Crypt_MathTest extends PHPUnit_Framework_TestCase + $this->assertTrue(bccomp($result, $lower) !== '-1'); + } + ++ public function testRandBytes() ++ { ++ for ($length = 1; $length < 4096; $length++) { ++ $rand = Zend_Crypt_Math::randBytes($length); ++ $this->assertTrue(false !== $rand); ++ $this->assertEquals($length, strlen($rand)); ++ } ++ } ++ ++ public function testRandInteger() ++ { ++ for ($i = 0; $i < 1024; $i++) { ++ $min = rand(1, PHP_INT_MAX/2); ++ $max = $min + rand(1, PHP_INT_MAX/2 - 1); ++ $rand = Zend_Crypt_Math::randInteger($min, $max); ++ $this->assertGreaterThanOrEqual($min, $rand); ++ $this->assertLessThanOrEqual($max, $rand); ++ } ++ } ++ ++ public static function provideRandInt() ++ { ++ return [ ++ [2, 1, 10000, 100, 0.9, 1.1, false], ++ [2, 1, 10000, 100, 0.8, 1.2, true] ++ ]; ++ } ++ ++ /** ++ * A Monte Carlo test that generates $cycles numbers from 0 to $tot ++ * and test if the numbers are above or below the line y=x with a ++ * frequency range of [$min, $max] ++ * ++ * @dataProvider provideRandInt ++ */ ++ public function testMontecarloRandInteger($num, $valid, $cycles, $tot, $min, $max, $strong) ++ { ++ try { ++ $test = Zend_Crypt_Math::randBytes(1, $strong); ++ } catch (Zend_Crypt_Exception $e) { ++ $this->markTestSkipped($e->getMessage()); ++ } ++ ++ $i = 0; ++ $count = 0; ++ do { ++ $up = 0; ++ $down = 0; ++ for ($i = 0; $i < $cycles; $i++) { ++ $x = Zend_Crypt_Math::randInteger(0, $tot, $strong); ++ $y = Zend_Crypt_Math::randInteger(0, $tot, $strong); ++ if ($x > $y) { ++ $up++; ++ } elseif ($x < $y) { ++ $down++; ++ } ++ } ++ $this->assertGreaterThan(0, $up); ++ $this->assertGreaterThan(0, $down); ++ $ratio = $up / $down; ++ if ($ratio > $min && $ratio < $max) { ++ $count++; ++ } ++ $i++; ++ } while ($i < $num && $count < $valid); ++ ++ if ($count < $valid) { ++ $this->fail('The random number generator failed the Monte Carlo test'); ++ } ++ } + } diff --git a/debian/patches/series b/debian/patches/series index 6c2191f..f9d3a80 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -5,3 +5,4 @@ 0005-ZF2015-06-Fix-potential-XXE-vector-via-BOM-detection.patch 0006-ZF2015-07-Use-umask-of-0002.patch 0007-ZF2015-08-Fix-null-byte-injection-for-PDO-MsSql.patch +0008-ZF2015-09-Fixed-entropy-issue-in-word-CAPTCHA.patch
diff --git a/library/Zend/Captcha/Word.php b/library/Zend/Captcha/Word.php index 1f0e0fc..ba39580 100644 --- a/library/Zend/Captcha/Word.php +++ b/library/Zend/Captcha/Word.php @@ -22,6 +22,9 @@ /** @see Zend_Captcha_Base */ require_once 'Zend/Captcha/Base.php'; +/** @see Zend_Crypt_Math */ +require_once 'Zend/Crypt/Math.php'; + /** * Word-based captcha adapter * @@ -39,10 +42,10 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base /**#@+ * @var array Character sets */ - static $V = array("a", "e", "i", "o", "u", "y"); - static $VN = array("a", "e", "i", "o", "u", "y","2","3","4","5","6","7","8","9"); - static $C = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z"); - static $CN = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z","2","3","4","5","6","7","8","9"); + static public $V = array("a", "e", "i", "o", "u", "y"); + static public $VN = array("a", "e", "i", "o", "u", "y","2","3","4","5","6","7","8","9"); + static public $C = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z"); + static public $CN = array("b","c","d","f","g","h","j","k","m","n","p","q","r","s","t","u","v","w","x","z","2","3","4","5","6","7","8","9"); /**#@-*/ /** @@ -326,10 +329,12 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base $vowels = $this->_useNumbers ? self::$VN : self::$V; $consonants = $this->_useNumbers ? self::$CN : self::$C; + $totIndexCon = count($consonants) - 1; + $totIndexVow = count($vowels) - 1; for ($i=0; $i < $wordLen; $i = $i + 2) { // generate word with mix of vowels and consonants - $consonant = $consonants[array_rand($consonants)]; - $vowel = $vowels[array_rand($vowels)]; + $consonant = $consonants[Zend_Crypt_Math::randInteger(0, $totIndexCon, true)]; + $vowel = $vowels[Zend_Crypt_Math::randInteger(0, $totIndexVow, true)]; $word .= $consonant . $vowel; } @@ -359,7 +364,7 @@ abstract class Zend_Captcha_Word extends Zend_Captcha_Base protected function _generateRandomId() { - return md5(mt_rand(0, 1000) . microtime(true)); + return md5(Zend_Crypt_Math::randBytes(32)); } /** diff --git a/library/Zend/Crypt/Math.php b/library/Zend/Crypt/Math.php index 40395f5..8882259 100644 --- a/library/Zend/Crypt/Math.php +++ b/library/Zend/Crypt/Math.php @@ -65,13 +65,101 @@ class Zend_Crypt_Math extends Zend_Crypt_Math_BigInteger } /** + * Return a random strings of $length bytes + * + * @param integer $length + * @param boolean $strong + * @return string + */ + public static function randBytes($length, $strong = false) + { + $length = (int) $length; + if ($length <= 0) { + return false; + } + if (function_exists('openssl_random_pseudo_bytes')) { + $bytes = openssl_random_pseudo_bytes($length, $usable); + if ($strong === $usable) { + return $bytes; + } + } + if (function_exists('mcrypt_create_iv')) { + $bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + if ($bytes !== false && strlen($bytes) === $length) { + return $bytes; + } + } + if (file_exists('/dev/urandom') && is_readable('/dev/urandom')) { + $frandom = fopen('/dev/urandom', 'r'); + if ($frandom !== false) { + return fread($frandom, $length); + } + } + if (true === $strong) { + require_once 'Zend/Crypt/Exception.php'; + throw new Zend_Crypt_Exception( + 'This PHP environment doesn\'t support secure random number generation. ' . + 'Please consider installing the OpenSSL and/or Mcrypt extensions' + ); + } + $rand = ''; + for ($i = 0; $i < $length; $i++) { + $rand .= chr(mt_rand(0, 255)); + } + return $rand; + } + + /** + * Return a random integer between $min and $max + * + * @param integer $min + * @param integer $max + * @param boolean $strong + * @return integer + */ + public static function randInteger($min, $max, $strong = false) + { + if ($min > $max) { + require_once 'Zend/Crypt/Exception.php'; + throw new Zend_Crypt_Exception( + 'The min parameter must be lower than max parameter' + ); + } + $range = $max - $min; + if ($range == 0) { + return $max; + } elseif ($range > PHP_INT_MAX || is_float($range)) { + require_once 'Zend/Crypt/Exception.php'; + throw new Zend_Crypt_Exception( + 'The supplied range is too great to generate' + ); + } + // calculate number of bits required to store range on this machine + $r = $range; + $bits = 0; + while ($r) { + $bits++; + $r >>= 1; + } + $bits = (int) max($bits, 1); + $bytes = (int) max(ceil($bits / 8), 1); + $filter = (int) ((1 << $bits) - 1); + do { + $rnd = hexdec(bin2hex(self::randBytes($bytes, $strong))); + $rnd &= $filter; + } while ($rnd > $range); + return ($min + $rnd); + } + + /** * Get the big endian two's complement of a given big integer in * binary notation * * @param string $long * @return string */ - public function btwoc($long) { + public function btwoc($long) + { if (ord($long[0]) > 127) { return "\x00" . $long; } diff --git a/tests/Zend/Crypt/MathTest.php b/tests/Zend/Crypt/MathTest.php index eeb9325..79bd02e 100644 --- a/tests/Zend/Crypt/MathTest.php +++ b/tests/Zend/Crypt/MathTest.php @@ -21,7 +21,7 @@ */ require_once 'Zend/Crypt/Math.php'; - +require_once 'Zend/Crypt/Exception.php'; /** * @category Zend @@ -59,4 +58,74 @@ class Zend_Crypt_MathTest extends PHPUnit_Framework_TestCase $this->assertTrue(bccomp($result, $lower) !== '-1'); } + public function testRandBytes() + { + for ($length = 1; $length < 4096; $length++) { + $rand = Zend_Crypt_Math::randBytes($length); + $this->assertTrue(false !== $rand); + $this->assertEquals($length, strlen($rand)); + } + } + + public function testRandInteger() + { + for ($i = 0; $i < 1024; $i++) { + $min = rand(1, PHP_INT_MAX/2); + $max = $min + rand(1, PHP_INT_MAX/2 - 1); + $rand = Zend_Crypt_Math::randInteger($min, $max); + $this->assertGreaterThanOrEqual($min, $rand); + $this->assertLessThanOrEqual($max, $rand); + } + } + + public static function provideRandInt() + { + return [ + [2, 1, 10000, 100, 0.9, 1.1, false], + [2, 1, 10000, 100, 0.8, 1.2, true] + ]; + } + + /** + * A Monte Carlo test that generates $cycles numbers from 0 to $tot + * and test if the numbers are above or below the line y=x with a + * frequency range of [$min, $max] + * + * @dataProvider provideRandInt + */ + public function testMontecarloRandInteger($num, $valid, $cycles, $tot, $min, $max, $strong) + { + try { + $test = Zend_Crypt_Math::randBytes(1, $strong); + } catch (Zend_Crypt_Exception $e) { + $this->markTestSkipped($e->getMessage()); + } + + $i = 0; + $count = 0; + do { + $up = 0; + $down = 0; + for ($i = 0; $i < $cycles; $i++) { + $x = Zend_Crypt_Math::randInteger(0, $tot, $strong); + $y = Zend_Crypt_Math::randInteger(0, $tot, $strong); + if ($x > $y) { + $up++; + } elseif ($x < $y) { + $down++; + } + } + $this->assertGreaterThan(0, $up); + $this->assertGreaterThan(0, $down); + $ratio = $up / $down; + if ($ratio > $min && $ratio < $max) { + $count++; + } + $i++; + } while ($i < $num && $count < $valid); + + if ($count < $valid) { + $this->fail('The random number generator failed the Monte Carlo test'); + } + } }
signature.asc
Description: PGP signature