[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#806165: jessie-pu: package zendframework/1.12.9+dfsg-2+deb8u5



Package: release.debian.org
Severity: normal
Tags: jessie
User: release.debian.org@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 <taffit@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.zimuel@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');
+        }
+    }
 }

Attachment: signature.asc
Description: PGP signature


Reply to: