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) --- program/lib/Roundcube/rcube_db.php | 218 ++++++++++++++++++++++++++++++++--------------------- 1 files changed, 131 insertions(+), 87 deletions(-) diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php index 5d8c4a5..b3f549b 100644 --- a/program/lib/Roundcube/rcube_db.php +++ b/program/lib/Roundcube/rcube_db.php @@ -2,8 +2,6 @@ /** +-----------------------------------------------------------------------+ - | program/include/rcube_db.php | - | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | | | @@ -13,19 +11,17 @@ | | | PURPOSE: | | Database wrapper class that implements PHP PDO functions | - | | +-----------------------------------------------------------------------+ | Author: Aleksander Machniak <alec@alec.pl> | +-----------------------------------------------------------------------+ */ - /** * Database independent query interface. * This is a wrapper for the PHP PDO. * * @package Framework - * @sbpackage Database + * @subpackage Database */ class rcube_db { @@ -37,12 +33,11 @@ protected $db_mode; // Connection mode protected $dbh; // Connection handle - protected $db_error = false; - protected $db_error_msg = ''; - protected $conn_failure = false; - protected $a_query_results = array('dummy'); - protected $last_res_id = 0; - protected $db_index = 0; + protected $db_error = false; + protected $db_error_msg = ''; + protected $conn_failure = false; + protected $db_index = 0; + protected $last_result; protected $tables; protected $variables; @@ -75,7 +70,7 @@ $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver; $class = "rcube_db_$driver"; - if (!class_exists($class)) { + if (!$driver || !class_exists($class)) { rcube::raise_error(array('code' => 600, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__, 'message' => "Configuration error. Unsupported database driver: $driver"), @@ -133,7 +128,7 @@ $dsn_string = $this->dsn_string($dsn); $dsn_options = $this->dsn_options($dsn); - if ($db_pconn) { + if ($this->db_pconn) { $dsn_options[PDO::ATTR_PERSISTENT] = true; } @@ -267,14 +262,14 @@ /** * Getter for error state * - * @param int $res_id Optional query result identifier + * @param mixed $result Optional query result * * @return string Error message */ - public function is_error($res_id = null) + public function is_error($result = null) { - if ($res_id !== null) { - return $this->_get_result($res_id) === false ? $this->db_error_msg : null; + if ($result !== null) { + return $result === false ? $this->db_error_msg : null; } return $this->db_error ? $this->db_error_msg : null; @@ -343,7 +338,7 @@ * @param int Number of rows for LIMIT statement * @param mixed Values to be inserted in query * - * @return int Query handle identifier + * @return PDOStatement|bool Query handle or False on error */ public function limitquery() { @@ -363,7 +358,7 @@ * @param int $numrows Number of rows for LIMIT statement * @param array $params Values to be inserted in query * - * @return int Query handle identifier + * @return PDOStatement|bool Query handle or False on error */ protected function _query($query, $offset, $numrows, $params) { @@ -374,7 +369,7 @@ // check connection before proceeding if (!$this->is_connected()) { - return null; + return $this->last_result = false; } if ($numrows || $offset) { @@ -405,36 +400,69 @@ $this->debug($query); - $query = $this->dbh->query($query); + // destroy reference to previous result, required for SQLite driver (#1488874) + $this->last_result = null; + $this->db_error_msg = null; - if ($query === false) { + // send query + $result = $this->dbh->query($query); + + if ($result === false) { $error = $this->dbh->errorInfo(); $this->db_error = true; $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); rcube::raise_error(array('code' => 500, 'type' => 'db', 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg), true, false); + 'message' => $this->db_error_msg . " (SQL Query: $query)" + ), true, false); } - // add result, even if it's an error - return $this->_add_result($query); + $this->last_result = $result; + + return $result; } /** * Get number of affected rows for the last query * - * @param number $res_id Optional query handle identifier + * @param mixed $result Optional query handle * - * @return int Number of rows or false on failure + * @return int Number of (matching) rows */ - public function affected_rows($res_id = null) + public function affected_rows($result = null) { - if ($result = $this->_get_result($res_id)) { + if ($result || ($result === null && ($result = $this->last_result))) { return $result->rowCount(); } return 0; + } + + /** + * Get number of rows for a SQL query + * If no query handle is specified, the last query will be taken as reference + * + * @param mixed $result Optional query handle + * @return mixed Number of rows or false on failure + * @deprecated This method shows very poor performance and should be avoided. + */ + public function num_rows($result = null) + { + if ($result || ($result === null && ($result = $this->last_result))) { + // repeat query with SELECT COUNT(*) ... + if (preg_match('/^SELECT\s+(?:ALL\s+|DISTINCT\s+)?(?:.*?)\s+FROM\s+(.*)$/ims', $result->queryString, $m)) { + $query = $this->dbh->query('SELECT COUNT(*) FROM ' . $m[1], PDO::FETCH_NUM); + return $query ? intval($query->fetchColumn(0)) : false; + } + else { + $num = count($result->fetchAll()); + $result->execute(); // re-execute query because there's no seek(0) + return $num; + } + } + + return false; } /** @@ -464,13 +492,12 @@ * Get an associative array for one row * If no query handle is specified, the last query will be taken as reference * - * @param int $res_id Optional query handle identifier + * @param mixed $result Optional query handle * * @return mixed Array with col values or false on failure */ - public function fetch_assoc($res_id = null) + public function fetch_assoc($result = null) { - $result = $this->_get_result($res_id); return $this->_fetch_row($result, PDO::FETCH_ASSOC); } @@ -478,31 +505,30 @@ * Get an index array for one row * If no query handle is specified, the last query will be taken as reference * - * @param int $res_id Optional query handle identifier + * @param mixed $result Optional query handle * * @return mixed Array with col values or false on failure */ - public function fetch_array($res_id = null) + public function fetch_array($result = null) { - $result = $this->_get_result($res_id); return $this->_fetch_row($result, PDO::FETCH_NUM); } /** * Get col values for a result row * - * @param PDOStatement $result Result handle - * @param int $mode Fetch mode identifier + * @param mixed $result Optional query handle + * @param int $mode Fetch mode identifier * * @return mixed Array with col values or false on failure */ protected function _fetch_row($result, $mode) { - if (!is_object($result) || !$this->is_connected()) { - return false; + if ($result || ($result === null && ($result = $this->last_result))) { + return $result->fetch($mode); } - return $result->fetch($mode); + return false; } /** @@ -538,8 +564,8 @@ if ($this->tables === null) { $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME'); - if ($res = $this->_get_result($q)) { - $this->tables = $res->fetchAll(PDO::FETCH_COLUMN, 0); + if ($q) { + $this->tables = $q->fetchAll(PDO::FETCH_COLUMN, 0); } else { $this->tables = array(); @@ -561,8 +587,8 @@ $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?', array($table)); - if ($res = $this->_get_result($q)) { - return $res->fetchAll(PDO::FETCH_COLUMN, 0); + if ($q) { + return $q->fetchAll(PDO::FETCH_COLUMN, 0); } return array(); @@ -572,7 +598,7 @@ * Formats input so it can be safely used in a query * * @param mixed $input Value to quote - * @param string $type Type of data + * @param string $type Type of data (integer, bool, ident) * * @return string Quoted/converted string for use in query */ @@ -585,6 +611,10 @@ if (is_null($input)) { return 'NULL'; + } + + if ($type == 'ident') { + return $this->quote_identifier($input); } // create DB handle if not available @@ -605,6 +635,22 @@ } /** + * Escapes a string so it can be safely used in a query + * + * @param string $str A string to escape + * + * @return string Escaped string for use in a query + */ + public function escape($str) + { + if (is_null($str)) { + return 'NULL'; + } + + return substr($this->quote($str), 1, -1); + } + + /** * Quotes a string so it can be safely used as a table or column name * * @param string $str Value to quote @@ -616,6 +662,20 @@ public function quoteIdentifier($str) { return $this->quote_identifier($str); + } + + /** + * Escapes a string so it can be safely used in a query + * + * @param string $str A string to escape + * + * @return string Escaped string for use in a query + * @deprecated Replaced by rcube_db::escape + * @see rcube_db::escape + */ + public function escapeSimple($str) + { + return $this->escape($str); } /** @@ -636,7 +696,7 @@ $name[] = $start . $elem . $end; } - return implode($name, '.'); + return implode($name, '.'); } /** @@ -653,7 +713,7 @@ * Return list of elements for use with SQL's IN clause * * @param array $arr Input array - * @param string $type Type of data + * @param string $type Type of data (integer, bool, ident) * * @return string Comma-separated list of quoted values for use in query */ @@ -729,12 +789,19 @@ /** * Encodes non-UTF-8 characters in string/array/object (recursive) * - * @param mixed $input Data to fix + * @param mixed $input Data to fix + * @param bool $serialized Enable serialization * * @return mixed Properly UTF-8 encoded data */ - public static function encode($input) + public static function encode($input, $serialized = false) { + // use Base64 encoding to workaround issues with invalid + // or null characters in serialized string (#1489142) + if ($serialized) { + return base64_encode(serialize($input)); + } + if (is_object($input)) { foreach (get_object_vars($input) as $idx => $value) { $input->$idx = self::encode($value); @@ -745,6 +812,7 @@ foreach ($input as $idx => $value) { $input[$idx] = self::encode($value); } + return $input; } @@ -754,12 +822,24 @@ /** * Decodes encoded UTF-8 string/object/array (recursive) * - * @param mixed $input Input data + * @param mixed $input Input data + * @param bool $serialized Enable serialization * * @return mixed Decoded data */ - public static function decode($input) + public static function decode($input, $serialized = false) { + // use Base64 encoding to workaround issues with invalid + // or null characters in serialized string (#1489142) + if ($serialized) { + // Keep backward compatybility where base64 wasn't used + if (strpos(substr($input, 0, 16), ':') !== false) { + return self::decode(@unserialize($input)); + } + + return @unserialize(base64_decode($input)); + } + if (is_object($input)) { foreach (get_object_vars($input) as $idx => $value) { $input->$idx = self::decode($value); @@ -774,42 +854,6 @@ } return utf8_decode($input); - } - - /** - * Adds a query result and returns a handle ID - * - * @param object $res Query handle - * - * @return int Handle ID - */ - protected function _add_result($res) - { - $this->last_res_id = sizeof($this->a_query_results); - $this->a_query_results[$this->last_res_id] = $res; - - return $this->last_res_id; - } - - /** - * Resolves a given handle ID and returns the according query handle - * If no ID is specified, the last resource handle will be returned - * - * @param int $res_id Handle ID - * - * @return mixed Resource handle or false on failure - */ - protected function _get_result($res_id = null) - { - if ($res_id == null) { - $res_id = $this->last_res_id; - } - - if (!empty($this->a_query_results[$res_id])) { - return $this->a_query_results[$res_id]; - } - - return false; } /** -- Gitblit v1.9.1