From 037af6890fe6fdb84a08d3c86083e847c90ec0ad Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Tue, 22 Oct 2013 08:17:26 -0400
Subject: [PATCH] Fix vulnerability in handling _session argument of utils/save-prefs (#1489382)

---
 plugins/managesieve/lib/Net/Sieve.php |  170 ++++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 116 insertions(+), 54 deletions(-)

diff --git a/plugins/managesieve/lib/Net/Sieve.php b/plugins/managesieve/lib/Net/Sieve.php
index be52540..8a0a9b0 100644
--- a/plugins/managesieve/lib/Net/Sieve.php
+++ b/plugins/managesieve/lib/Net/Sieve.php
@@ -39,7 +39,7 @@
  * @copyright 2002-2003 Richard Heyes
  * @copyright 2006-2008 Anish Mistry
  * @license   http://www.opensource.org/licenses/bsd-license.php BSD
- * @version   SVN: $Id: Sieve.php 289313 2009-10-07 22:26:33Z yunosh $
+ * @version   SVN: $Id: Sieve.php 300898 2010-07-01 09:49:02Z yunosh $
  * @link      http://pear.php.net/package/Net_Sieve
  */
 
@@ -83,7 +83,7 @@
  * @copyright 2002-2003 Richard Heyes
  * @copyright 2006-2008 Anish Mistry
  * @license   http://www.opensource.org/licenses/bsd-license.php BSD
- * @version   Release: @package_version@
+ * @version   Release: 1.3.0
  * @link      http://pear.php.net/package/Net_Sieve
  * @link      http://www.ietf.org/rfc/rfc3028.txt RFC 3028 (Sieve: A Mail
  *            Filtering Language)
@@ -107,7 +107,7 @@
      *
      * @var array
      */
-    var $_supportedSASLAuthMethods = array('DIGEST-MD5', 'CRAM-MD5');
+    var $supportedSASLAuthMethods = array('DIGEST-MD5', 'CRAM-MD5');
 
     /**
      * The socket handle.
@@ -204,11 +204,13 @@
      * @param boolean $useTLS     Use TLS if available.
      * @param array   $options    Additional options for
      *                            stream_context_create().
+     * @param mixed   $handler    A callback handler for the debug output.
      */
     function Net_Sieve($user = null, $pass  = null, $host = 'localhost',
-        $port = 2000, $logintype = '', $euser = '', $debug = false,
-        $bypassAuth = false, $useTLS = true, $options = null
-    ) {
+                       $port = 2000, $logintype = '', $euser = '',
+                       $debug = false, $bypassAuth = false, $useTLS = true,
+                       $options = null, $handler = null)
+    {
         $this->_state             = NET_SIEVE_STATE_DISCONNECTED;
         $this->_data['user']      = $user;
         $this->_data['pass']      = $pass;
@@ -217,10 +219,10 @@
         $this->_data['logintype'] = $logintype;
         $this->_data['euser']     = $euser;
         $this->_sock              = new Net_Socket();
-        $this->_debug             = $debug;
         $this->_bypassAuth        = $bypassAuth;
         $this->_useTLS            = $useTLS;
         $this->_options           = $options;
+        $this->setDebug($debug, $handler);
 
         /* Try to include the Auth_SASL package.  If the package is not
          * available, we disable the authentication methods that depend upon
@@ -294,6 +296,13 @@
      */
     function connect($host, $port, $options = null, $useTLS = true)
     {
+        $this->_data['host'] = $host;
+        $this->_data['port'] = $port;
+        $this->_useTLS       = $useTLS;
+        if (!empty($options) && is_array($options)) {
+            $this->_options = array_merge($this->_options, $options);
+        }
+
         if (NET_SIEVE_STATE_DISCONNECTED != $this->_state) {
             return PEAR::raiseError('Not currently in DISCONNECTED state', 1);
         }
@@ -357,6 +366,12 @@
      */
     function login($user, $pass, $logintype = null, $euser = '', $bypassAuth = false)
     {
+        $this->_data['user']      = $user;
+        $this->_data['pass']      = $pass;
+        $this->_data['logintype'] = $logintype;
+        $this->_data['euser']     = $euser;
+        $this->_bypassAuth        = $bypassAuth;
+
         if (NET_SIEVE_STATE_AUTHORISATION != $this->_state) {
             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
         }
@@ -473,7 +488,9 @@
         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
             return PEAR::raiseError('Not currently in TRANSACTION state', 1);
         }
-        if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE "%s" %d', $scriptname, $size)))) {
+
+        $command = sprintf('HAVESPACE %s %d', $this->_escape($scriptname), $size);
+        if (PEAR::isError($res = $this->_doCmd($command))) {
             return $res;
         }
         return true;
@@ -635,10 +652,10 @@
         if (PEAR::isError($result = $this->_sendCmd('AUTHENTICATE "LOGIN"'))) {
             return $result;
         }
-        if (PEAR::isError($result = $this->_doCmd('"' . base64_encode($user) . '"'))) {
+        if (PEAR::isError($result = $this->_doCmd('"' . base64_encode($user) . '"', true))) {
             return $result;
         }
-        return $this->_doCmd('"' . base64_encode($pass) . '"');
+        return $this->_doCmd('"' . base64_encode($pass) . '"', true);
     }
 
     /**
@@ -687,10 +704,10 @@
             return $response;
         }
 
-        if (PEAR::isError($result = $this->_sendStringResponse(base64_encode($param)))) {
+        if (PEAR::isError($result = $this->_sendStringResponse(base64_encode($response)))) {
             return $result;
         }
-        if (PEAR::isError($result = $this->_doCmd())) {
+        if (PEAR::isError($result = $this->_doCmd('', true))) {
             return $result;
         }
         if ($this->_toUpper(substr($result, 0, 2)) == 'OK') {
@@ -738,7 +755,9 @@
         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
         }
-        if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT "%s"', $scriptname)))) {
+
+        $command = sprintf('DELETESCRIPT %s', $this->_escape($scriptname));
+        if (PEAR::isError($res = $this->_doCmd($command))) {
             return $res;
         }
         return true;
@@ -757,11 +776,12 @@
             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
         }
 
-        if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT "%s"', $scriptname)))) {
+        $command = sprintf('GETSCRIPT %s', $this->_escape($scriptname));
+        if (PEAR::isError($res = $this->_doCmd($command))) {
             return $res;
         }
 
-        return preg_replace('/{[0-9]+}\r\n/', '', $res);
+        return preg_replace('/^{[0-9]+}\r\n/', '', $res);
     }
 
     /**
@@ -777,9 +797,12 @@
         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
         }
-        if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE "%s"', $scriptname)))) {
+
+        $command = sprintf('SETACTIVE %s', $this->_escape($scriptname));
+        if (PEAR::isError($res = $this->_doCmd($command))) {
             return $res;
         }
+
         $this->_activeScript = $scriptname;
         return true;
     }
@@ -806,9 +829,10 @@
         $res = explode("\r\n", $res);
         foreach ($res as $value) {
             if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) {
-                $scripts[] = $matches[1];
+                $script_name = stripslashes($matches[1]);
+                $scripts[] = $script_name;
                 if (!empty($matches[2])) {
-                    $activescript = $matches[1];
+                    $activescript = $script_name;
                 }
             }
         }
@@ -831,8 +855,10 @@
         }
 
         $stringLength = $this->_getLineLength($scriptdata);
+        $command      = sprintf("PUTSCRIPT %s {%d+}\r\n%s",
+            $this->_escape($scriptname), $stringLength, $scriptdata);
 
-        if (PEAR::isError($res = $this->_doCmd(sprintf("PUTSCRIPT \"%s\" {%d+}\r\n%s", $scriptname, $stringLength, $scriptdata)))) {
+        if (PEAR::isError($res = $this->_doCmd($command))) {
             return $res;
         }
 
@@ -979,6 +1005,28 @@
     }
 
     /**
+     * Receives x bytes from the server.
+     *
+     * @param int $length  Number of bytes to read
+     *
+     * @return string  The server response.
+     */
+    function _recvBytes($length)
+    {
+        $response = '';
+        $response_length = 0;
+
+        while ($response_length < $length) {
+            $response .= $this->_sock->read($length - $response_length);
+            $response_length = $this->_getLineLength($response);
+        }
+
+        $this->_debug("S: " . rtrim($response));
+
+        return $response;
+    }
+
+    /**
      * Send a command and retrieves a response from the server.
      *
      * @param string $cmd   The command to send.
@@ -1011,11 +1059,11 @@
 
                 if ('NO' == substr($uc_line, 0, 2)) {
                     // Check for string literal error message.
-                    if (preg_match('/^no {([0-9]+)\+?}/i', $line, $matches)) {
-                        $line .= str_replace(
-                            "\r\n", ' ', $this->_sock->read($matches[1] + 2)
-                        );
-                        $this->_debug("S: $line");
+                    if (preg_match('/{([0-9]+)}$/i', $line, $matches)) {
+                        $line = substr($line, 0, -(strlen($matches[1])+2))
+                            . str_replace(
+                                "\r\n", ' ', $this->_recvBytes($matches[1] + 2)
+                            );
                     }
                     return PEAR::raiseError(trim($response . substr($line, 2)), 3);
                 }
@@ -1050,16 +1098,11 @@
                     return PEAR::raiseError(trim($response . $line), 6);
                 }
 
+                // "\+?" is added in the regexp to workaround DBMail bug
+                // http://dbmail.org/mantis/view.php?id=963
                 if (preg_match('/^{([0-9]+)\+?}/i', $line, $matches)) {
-                    // Matches String Responses.
-                    $str_size = $matches[1] + 2;
-                    $line = '';
-                    $line_length = 0;
-                    while ($line_length < $str_size) {
-                        $line .= $this->_sock->read($str_size - $line_length);
-                        $line_length = $this->_getLineLength($line);
-                    }
-                    $this->_debug("S: $line");
+                    // Matches literal string responses.
+                    $line = $this->_recvBytes($matches[1] + 2);
 
                     if (!$auth) {
                         // Receive the pending OK only if we aren't
@@ -1099,33 +1142,30 @@
         if (!isset($this->_capability['sasl'])) {
             return PEAR::raiseError('This server doesn\'t support any authentication methods. SASL problem?');
         }
-
-        $serverMethods = $this->_capability['sasl'];
+        if (!$this->_capability['sasl']) {
+            return PEAR::raiseError('This server doesn\'t support any authentication methods.');
+        }
 
         if ($userMethod) {
-            $methods = array($userMethod);
-        } else {
-            $methods = $this->supportedAuthMethods;
-        }
-
-        if (!$methods || !$serverMethods) {
+            if (in_array($userMethod, $this->_capability['sasl'])) {
+                return $userMethod;
+            }
             return PEAR::raiseError(
-                'This server doesn\'t support any authentication methods.'
-            );
+                sprintf('No supported authentication method found. The server supports these methods: %s, but we want to use: %s',
+                        implode(', ', $this->_capability['sasl']),
+                        $userMethod));
         }
 
-        foreach ($methods as $method) {
-            if (in_array($method, $serverMethods)) {
+        foreach ($this->supportedAuthMethods as $method) {
+            if (in_array($method, $this->_capability['sasl'])) {
                 return $method;
             }
         }
 
         return PEAR::raiseError(
-            'No supported authentication method found. The server supports these methods: '
-            . implode(',', $serverMethods)
-            . ', but we only support: '
-            . implode(',', $this->supportedAuthMethods)
-        );
+            sprintf('No supported authentication method found. The server supports these methods: %s, but we only support: %s',
+                    implode(', ', $this->_capability['sasl']),
+                    implode(', ', $this->supportedAuthMethods)));
     }
 
     /**
@@ -1147,7 +1187,13 @@
 
         // The server should be sending a CAPABILITY response after
         // negotiating TLS. Read it, and ignore if it doesn't.
-        $this->_doCmd();
+        // Doesn't work with older timsieved versions
+        $regexp = '/^CYRUS TIMSIEVED V([0-9.]+)/';
+        if (!preg_match($regexp, $this->_capability['implementation'], $matches)
+            || version_compare($matches[1], '2.3.10', '>=')
+        ) {
+            $this->_doCmd();
+        }
 
         // RFC says we need to query the server capabilities again now that we
         // are under encryption.
@@ -1169,9 +1215,7 @@
      */
     function _getLineLength($string)
     {
-        if (extension_loaded('mbstring')
-            || @dl(PHP_SHLIB_PREFIX . 'mbstring.' . PHP_SHLIB_SUFFIX)
-        ) {
+        if (extension_loaded('mbstring')) {
             return mb_strlen($string, 'latin1');
         } else {
             return strlen($string);
@@ -1195,6 +1239,24 @@
     }
 
     /**
+     * Convert string into RFC's quoted-string or literal-c2s form
+     *
+     * @param string $string The string to convert.
+     *
+     * @return string Result string
+     */
+    function _escape($string)
+    {
+        // Some implementations doesn't allow UTF-8 characters in quoted-string
+        // It's safe to use literal-c2s
+        if (preg_match('/[^\x01-\x09\x0B-\x0C\x0E-\x7F]/', $string)) {
+            return sprintf("{%d+}\r\n%s", $this->_getLineLength($string), $string);
+        }
+
+        return '"' . addcslashes($string, '\\"') . '"';
+    }
+
+    /**
      * Write debug text to the current debug output handler.
      *
      * @param string $message Debug message text.

--
Gitblit v1.9.1