From 197203727417a03d87053a47e5aa5175a76e3e0b Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Thu, 17 Oct 2013 04:24:53 -0400 Subject: [PATCH] Fix vulnerability in handling _session argument of utils/save-prefs (#1489382) --- program/include/rcube_imap.php | 268 ++++++++++++++++++++++++++++++++++++++++------------- 1 files changed, 200 insertions(+), 68 deletions(-) diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php index 8dfffe8..f06a8ee 100644 --- a/program/include/rcube_imap.php +++ b/program/include/rcube_imap.php @@ -32,7 +32,6 @@ */ class rcube_imap { - public $debug_level = 1; public $skip_deleted = false; public $page_size = 10; public $list_page = 1; @@ -318,6 +317,19 @@ /** + * Activate/deactivate debug mode + * + * @param boolean $dbg True if IMAP conversation should be logged + * @access public + */ + function set_debug($dbg = true) + { + $this->options['debug'] = $dbg; + $this->conn->setDebug($dbg, array($this, 'debug_handler')); + } + + + /** * Set default message charset * * This will be used for message decoding if a charset specification is not available @@ -466,7 +478,7 @@ */ function get_mailbox_name() { - return $this->conn->connected() ? $this->mailbox : ''; + return $this->mailbox; } @@ -675,8 +687,8 @@ { $mode = strtoupper($mode); - // count search set - if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) { + // count search set, assume search set is always up-to-date (don't check $force flag) + if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS')) { if ($this->search_threads) return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']); else @@ -1361,6 +1373,11 @@ $this->_messagecount($mailbox, 'ALL', true); $result = 0; + + if (empty($old)) { + return $result; + } + $new = $this->get_folder_stats($mailbox); // got new messages @@ -1508,7 +1525,7 @@ // I didn't found that SEARCH should return sorted IDs if (is_array($a_index)) sort($a_index); - } else if ($max = $this->_messagecount($mailbox)) { + } else if ($max = $this->_messagecount($mailbox, 'ALL', true, false)) { $a_index = range(1, $max); } @@ -1619,6 +1636,7 @@ * @param string $sort_field Header field to sort by * @return array search results as list of message IDs * @access public + * @todo: Search criteria should be provided in non-IMAP format, eg. array */ function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL) { @@ -1694,7 +1712,7 @@ } if ($orig_criteria == 'ALL') { - $max = $this->_messagecount($mailbox); + $max = $this->_messagecount($mailbox, 'ALL', true, false); $a_messages = $max ? range(1, $max) : array(); } else { @@ -1704,7 +1722,7 @@ // Error, try with US-ASCII (some servers may support only US-ASCII) if ($a_messages === false && $charset && $charset != 'US-ASCII') $a_messages = $this->conn->search($mailbox, - 'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset)); + $this->convert_criteria($criteria, $charset)); // I didn't found that SEARCH should return sorted IDs if (is_array($a_messages) && !$this->sort_field) @@ -1758,9 +1776,9 @@ $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n $string = substr($str, $string_offset - 1, $m[0]); $string = rcube_charset_convert($string, $charset, $dest_charset); - if (!$string) + if ($string === false) continue; - $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string); + $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string); $last = $m[0] + $string_offset - 1; } if ($last < strlen($str)) @@ -1961,6 +1979,10 @@ } $headers = $this->get_headers($uid, $mailbox); + + // message doesn't exist? + if (empty($headers)) + return null; // structure might be cached if (!empty($headers->structure)) @@ -2320,9 +2342,14 @@ // decode filename if (!empty($filename_mime)) { - $part->filename = rcube_imap::decode_mime_string($filename_mime, - $part->charset ? $part->charset : ($this->struct_charset ? $this->struct_charset : - rc_detect_encoding($filename_mime, $this->default_charset))); + if (!empty($part->charset)) + $charset = $part->charset; + else if (!empty($this->struct_charset)) + $charset = $this->struct_charset; + else + $charset = rc_detect_encoding($filename_mime, $this->default_charset); + + $part->filename = rcube_imap::decode_mime_string($filename_mime, $charset); } else if (!empty($filename_encoded)) { // decode filename according to RFC 2231, Section 4 @@ -2330,6 +2357,7 @@ $filename_charset = $fmatches[1]; $filename_encoded = $fmatches[2]; } + $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset); } } @@ -2366,24 +2394,22 @@ */ function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false) { - // get part encoding if not provided + // get part data if not provided if (!is_object($o_part)) { $structure = $this->conn->getStructure($this->mailbox, $uid, true); + $part_data = rcube_imap_generic::getStructurePartData($structure, $part); $o_part = new rcube_message_part; - $o_part->ctype_primary = strtolower(rcube_imap_generic::getStructurePartType($structure, $part)); - $o_part->encoding = strtolower(rcube_imap_generic::getStructurePartEncoding($structure, $part)); - $o_part->charset = rcube_imap_generic::getStructurePartCharset($structure, $part); + $o_part->ctype_primary = $part_data['type']; + $o_part->encoding = $part_data['encoding']; + $o_part->charset = $part_data['charset']; + $o_part->size = $part_data['size']; } - // TODO: Add caching for message parts - - if (!$part) { - $part = 'TEXT'; + if ($o_part && $o_part->size) { + $body = $this->conn->handlePartBody($this->mailbox, $uid, true, + $part ? $part : 'TEXT', $o_part->encoding, $print, $fp); } - - $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part, - $o_part->encoding, $print, $fp); if ($fp || $print) { return true; @@ -2396,7 +2422,11 @@ if (!$skip_charset_conv) { if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') { - $o_part->charset = $this->default_charset; + // try to extract charset information from HTML meta tag (#1488125) + if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) + $o_part->charset = strtoupper($m[1]); + else + $o_part->charset = $this->default_charset; } $body = rcube_charset_convert($body, $o_part->charset); } @@ -2582,10 +2612,14 @@ // make sure mailbox exists if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) { - if (in_array($to_mbox, $this->default_folders)) - $this->create_mailbox($to_mbox, true); - else + if (in_array($to_mbox, $this->default_folders)) { + if (!$this->create_mailbox($to_mbox, true)) { + return false; + } + } + else { return false; + } } $config = rcmail::get_instance()->config; @@ -2663,10 +2697,14 @@ // make sure mailbox exists if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) { - if (in_array($to_mbox, $this->default_folders)) - $this->create_mailbox($to_mbox, true); - else + if (in_array($to_mbox, $this->default_folders)) { + if (!$this->create_mailbox($to_mbox, true)) { + return false; + } + } + else { return false; + } } // copy messages @@ -2967,14 +3005,14 @@ /** - * Private method for mailbox listing + * Private method for mailbox listing (LSUB) * * @param string $root Optional root folder * @param string $name Optional name pattern * @param mixed $filter Optional filter * @param string $rights Optional ACL requirements * - * @return array List of mailboxes/folders + * @return array List of subscribed folders * @see rcube_imap::list_mailboxes() * @access private */ @@ -3002,9 +3040,10 @@ NULL, array('SUBSCRIBED')); // unsubscribe non-existent folders, remove from the list - if (is_array($a_folders) && $name == '*') { + // we can do this only when LIST response is available + if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { foreach ($a_folders as $idx => $folder) { - if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder]) + if (($opts = $this->conn->data['LIST'][$folder]) && in_array('\\NonExistent', $opts) ) { $this->conn->unsubscribe($folder); @@ -3017,11 +3056,12 @@ else { $a_folders = $this->conn->listSubscribed($root, $name); - // unsubscribe non-existent folders, remove from the list - if (is_array($a_folders) && $name == '*') { + // unsubscribe non-existent folders, remove them from the list, + // we can do this only when LIST response is available + if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { foreach ($a_folders as $idx => $folder) { - if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder]) - && in_array('\\Noselect', $opts) + if (!isset($this->conn->data['LIST'][$folder]) + || in_array('\\Noselect', $this->conn->data['LIST'][$folder]) ) { // Some servers returns \Noselect for existing folders if (!$this->mailbox_exists($folder)) { @@ -3055,7 +3095,19 @@ */ function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) { - // @TODO: caching + $cache_key = $root.':'.$name; + if (!empty($filter)) { + $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter)); + } + $cache_key .= ':'.$rights; + $cache_key = 'mailboxes.list.'.md5($cache_key); + + // get cached folder list + $a_mboxes = $this->get_cache($cache_key); + if (is_array($a_mboxes)) { + return $a_mboxes; + } + // Give plugins a chance to provide a list of mailboxes $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list', array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST')); @@ -3065,7 +3117,7 @@ } else { // retrieve list of folders from IMAP server - $a_mboxes = $this->conn->listMailboxes($root, $name); + $a_mboxes = $this->_list_unsubscribed($root, $name); } if (!is_array($a_mboxes)) { @@ -3075,6 +3127,11 @@ // INBOX should always be available if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) { array_unshift($a_mboxes, 'INBOX'); + } + + // cache folder attributes + if ($root == '' && $name == '*' && empty($filter)) { + $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']); } // filter folders list according to rights requirements @@ -3087,7 +3144,74 @@ $a_mboxes = $this->_sort_mailbox_list($a_mboxes); } + // write mailboxlist to cache + $this->update_cache($cache_key, $a_mboxes); + return $a_mboxes; + } + + + /** + * Private method for mailbox listing (LIST) + * + * @param string $root Optional root folder + * @param string $name Optional name pattern + * + * @return array List of folders + * @see rcube_imap::list_unsubscribed() + */ + private function _list_unsubscribed($root='', $name='*') + { + $result = $this->conn->listMailboxes($root, $name); + + if (!is_array($result)) { + return array(); + } + + // #1486796: some server configurations doesn't + // return folders in all namespaces, we'll try to detect that situation + // and ask for these namespaces separately + if ($root == '' && $name == '*') { + $delim = $this->get_hierarchy_delimiter(); + $namespace = $this->get_namespace(); + $search = array(); + + // build list of namespace prefixes + foreach ((array)$namespace as $ns) { + if (is_array($ns)) { + foreach ($ns as $ns_data) { + if (strlen($ns_data[0])) { + $search[] = $ns_data[0]; + } + } + } + } + + if (!empty($search)) { + // go through all folders detecting namespace usage + foreach ($result as $folder) { + foreach ($search as $idx => $prefix) { + if (strpos($folder, $prefix) === 0) { + unset($search[$idx]); + } + } + if (empty($search)) { + break; + } + } + + // get folders in hidden namespaces and add to the result + foreach ($search as $prefix) { + $list = $this->conn->listMailboxes($prefix, $name); + + if (!empty($list)) { + $result = array_merge($result, $list); + } + } + } + } + + return $result; } @@ -3363,8 +3487,8 @@ foreach ($this->namespace as $type => $namespace) { if (is_array($namespace)) { foreach ($namespace as $ns) { - if (strlen($ns[0])) { - if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1)) + if ($len = strlen($ns[0])) { + if (($len > 1 && $mailbox == substr($ns[0], 0, -1)) || strpos($mailbox, $ns[0]) === 0 ) { return $type; @@ -3418,30 +3542,29 @@ /** - * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors + * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors * * @param string $mailbox Folder name - * @param bool $force Set to True if options should be refreshed - * Options are available after LIST command only + * @param bool $force Set to True if attributes should be refreshed * * @return array Options list */ - function mailbox_options($mailbox, $force=false) + function mailbox_attributes($mailbox, $force=false) { - if ($mailbox == 'INBOX') { - return array(); + // get attributes directly from LIST command + if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$mailbox])) { + $opts = $this->conn->data['LIST'][$mailbox]; + } + // get cached folder attributes + else if (!$force) { + $opts = $this->get_cache('mailboxes.attributes'); + $opts = $opts[$mailbox]; } - if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) { - if ($force) { - $this->conn->listMailboxes('', $mailbox); - } - else { - return array(); - } + if (!is_array($opts)) { + $this->conn->listMailboxes('', $mailbox); + $opts = $this->conn->data['LIST'][$mailbox]; } - - $opts = $this->conn->data['LIST'][$mailbox]; return is_array($opts) ? $opts : array(); } @@ -3524,17 +3647,17 @@ } } - $options['name'] = $mailbox; - $options['options'] = $this->mailbox_options($mailbox, true); - $options['namespace'] = $this->mailbox_namespace($mailbox); - $options['rights'] = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array(); - $options['special'] = in_array($mailbox, $this->default_folders); + $options['name'] = $mailbox; + $options['attributes'] = $this->mailbox_attributes($mailbox, true); + $options['namespace'] = $this->mailbox_namespace($mailbox); + $options['rights'] = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array(); + $options['special'] = in_array($mailbox, $this->default_folders); - // Set 'noselect' and 'norename' flags - if (is_array($options['options'])) { - foreach ($options['options'] as $opt) { - $opt = strtolower($opt); - if ($opt == '\noselect' || $opt == '\nonexistent') { + // Set 'noselect' flag + if (is_array($options['attributes'])) { + foreach ($options['attributes'] as $attrib) { + $attrib = strtolower($attrib); + if ($attrib == '\noselect' || $attrib == '\nonexistent') { $options['noselect'] = true; } } @@ -3543,6 +3666,15 @@ $options['noselect'] = true; } + // Get folder rights (MYRIGHTS) + if ($acl && !$options['noselect']) { + // skip shared roots + if (!$options['is_root'] || $options['namespace'] == 'personal') { + $options['rights'] = (array)$this->my_rights($mailbox); + } + } + + // Set 'norename' flag if (!empty($options['rights'])) { $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']); @@ -4042,7 +4174,7 @@ $input = preg_replace("/\?=\s+=\?/", '?==?', $input); // encoded-word regexp - $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/'; + $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/'; // Find all RFC2047's encoded words if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { -- Gitblit v1.9.1