From acf633c73bc8df9a5036bc52d7568f4213ab73c7 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Fri, 06 May 2016 02:32:01 -0400
Subject: [PATCH] Fix XSS issue in href attribute on area tag (#5240, #5241)
---
program/lib/Roundcube/rcube_message.php | 249 ++++++++++++++++++++++++++++---------------------
1 files changed, 140 insertions(+), 109 deletions(-)
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index 3f14266..6268228 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -85,15 +85,16 @@
$this->headers = $this->storage->get_message($uid);
- if (!$this->headers)
+ if (!$this->headers) {
return;
+ }
$this->mime = new rcube_mime($this->headers->charset);
- $this->subject = $this->mime->decode_mime_string($this->headers->subject);
+ $this->subject = $this->headers->get('subject');
list(, $this->sender) = each($this->mime->decode_address_list($this->headers->from, 1));
- $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$uid]));
+ $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$this->folder.':'.$uid]));
$this->opt = array(
'safe' => $this->is_safe,
'prefer_html' => $this->app->config->get('prefer_html'),
@@ -125,15 +126,11 @@
*/
public function get_header($name, $raw = false)
{
- if (empty($this->headers))
+ if (empty($this->headers)) {
return null;
+ }
- if ($this->headers->$name)
- $value = $this->headers->$name;
- else if ($this->headers->others[$name])
- $value = $this->headers->others[$name];
-
- return $raw ? $value : $this->mime->decode_header($value);
+ return $this->headers->get($name, !$raw);
}
@@ -144,8 +141,7 @@
*/
public function set_safe($safe = true)
{
- $this->is_safe = $safe;
- $_SESSION['safe_messages'][$this->uid] = $this->is_safe;
+ $_SESSION['safe_messages'][$this->folder.':'.$this->uid] = $this->is_safe = $safe;
}
@@ -153,12 +149,13 @@
* Compose a valid URL for getting a message part
*
* @param string $mime_id Part MIME-ID
+ * @param mixed $embed Mimetype class for parts to be embedded
* @return string URL or false if part does not exist
*/
public function get_part_url($mime_id, $embed = false)
{
if ($this->mime_parts[$mime_id])
- return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1' : '');
+ return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1&_mimeclass=' . $embed : '');
else
return false;
}
@@ -171,10 +168,11 @@
* @param resource $fp File pointer to save the message part
* @param boolean $skip_charset_conv Disables charset conversion
* @param int $max_bytes Only read this number of bytes
+ * @param boolean $formatted Enables formatting of text/* parts bodies
*
* @return string Part content
*/
- public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0)
+ public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
{
if ($part = $this->mime_parts[$mime_id]) {
// stored in message structure (winmail/inline-uuencode)
@@ -188,47 +186,91 @@
// get from IMAP
$this->storage->set_folder($this->folder);
- return $this->storage->get_message_part($this->uid, $mime_id, $part, NULL, $fp, $skip_charset_conv, $max_bytes);
+ return $this->storage->get_message_part($this->uid, $mime_id, $part,
+ NULL, $fp, $skip_charset_conv, $max_bytes, $formatted);
}
}
/**
- * Determine if the message contains a HTML part
+ * Determine if the message contains a HTML part. This must to be
+ * a real part not an attachment (or its part)
*
- * @param bool $recursive Enables checking in all levels of the structure
- * @param bool $enriched Enables checking for text/enriched parts too
+ * @param bool $enriched Enables checking for text/enriched parts too
*
* @return bool True if a HTML is available, False if not
*/
- function has_html_part($recursive = true, $enriched = false)
+ function has_html_part($enriched = false)
{
// check all message parts
- foreach ($this->parts as $part) {
+ foreach ($this->mime_parts as $part) {
if ($part->mimetype == 'text/html' || ($enriched && $part->mimetype == 'text/enriched')) {
- // Level check, we'll skip e.g. HTML attachments
- if (!$recursive) {
- $level = explode('.', $part->mime_id);
+ // Skip if part is an attachment, don't use is_attachment() here
+ if ($part->filename) {
+ continue;
+ }
- // Skip if part is an attachment
- if ($this->is_attachment($part)) {
- continue;
+ $level = explode('.', $part->mime_id);
+ $depth = count($level);
+
+ // Check if the part belongs to higher-level's multipart part
+ // this can be alternative/related/signed/encrypted or mixed
+ while (array_pop($level) !== null) {
+ $parent_depth = count($level);
+ if (!$parent_depth) {
+ return true;
}
- // Check if the part belongs to higher-level's alternative/related
- while (array_pop($level) !== null) {
- if (!count($level)) {
- return true;
- }
-
- $parent = $this->mime_parts[join('.', $level)];
- if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
- continue 2;
- }
+ $parent = $this->mime_parts[join('.', $level)];
+ if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $parent->mimetype)
+ || ($parent->mimetype == 'multipart/mixed' && $parent_depth < $depth - 1)) {
+ continue 2;
}
}
- return true;
+ if ($part->size) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Determine if the message contains a text/plain part. This must to be
+ * a real part not an attachment (or its part)
+ *
+ * @return bool True if a plain text part is available, False if not
+ */
+ function has_text_part()
+ {
+ // check all message parts
+ foreach ($this->mime_parts as $part) {
+ if ($part->mimetype == 'text/plain') {
+ // Skip if part is an attachment, don't use is_attachment() here
+ if ($part->filename) {
+ continue;
+ }
+
+ $level = explode('.', $part->mime_id);
+
+ // Check if the part belongs to higher-level's alternative/related
+ while (array_pop($level) !== null) {
+ if (!count($level)) {
+ return true;
+ }
+
+ $parent = $this->mime_parts[join('.', $level)];
+ if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
+ continue 2;
+ }
+ }
+
+ if ($part->size) {
+ return true;
+ }
}
}
@@ -324,7 +366,7 @@
// parse headers from message/rfc822 part
if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
- list($headers, $dump) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 8192));
+ list($headers, ) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 32768));
$structure->headers = rcube_mime::parse_headers($headers);
}
}
@@ -332,7 +374,8 @@
$mimetype = $structure->mimetype;
// show message headers
- if ($recursive && is_array($structure->headers) && (isset($structure->headers['subject']) || isset($structure->headers['from']))) {
+ if ($recursive && is_array($structure->headers) &&
+ (isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to'])) {
$c = new stdClass;
$c->type = 'headers';
$c->headers = $structure->headers;
@@ -394,17 +437,24 @@
continue;
}
+ // We've encountered (malformed) messages with more than
+ // one text/plain or text/html part here. There's no way to choose
+ // which one is better, so we'll display first of them and add
+ // others as attachments (#1489358)
+
// check if sub part is
if ($is_multipart)
$related_part = $p;
- else if ($sub_mimetype == 'text/plain')
+ else if ($sub_mimetype == 'text/plain' && !$plain_part)
$plain_part = $p;
- else if ($sub_mimetype == 'text/html')
+ else if ($sub_mimetype == 'text/html' && !$html_part)
$html_part = $p;
- else if ($sub_mimetype == 'text/enriched')
+ else if ($sub_mimetype == 'text/enriched' && !$enriched_part)
$enriched_part = $p;
- else
- $attach_part = $p;
+ else {
+ // add unsupported/unrecognized parts to attachments list
+ $this->attachments[] = $sub_part;
+ }
}
// parse related part (alternative part could be in here)
@@ -445,19 +495,6 @@
$this->parts[] = $c;
}
-
- // add html part as attachment
- if ($html_part !== null && $structure->parts[$html_part] !== $print_part) {
- $html_part = $structure->parts[$html_part];
- $html_part->mimetype = 'text/html';
-
- $this->attachments[] = $html_part;
- }
-
- // add unsupported/unrecognized parts to attachments list
- if ($attach_part) {
- $this->attachments[] = $structure->parts[$attach_part];
- }
}
// this is an ecrypted message -> create a plaintext body with the according message
else if ($mimetype == 'multipart/encrypted') {
@@ -488,23 +525,16 @@
$mail_part = &$structure->parts[$i];
$primary_type = $mail_part->ctype_primary;
$secondary_type = $mail_part->ctype_secondary;
+ $part_mimetype = $mail_part->mimetype;
- // real content-type of message/rfc822
- if ($mail_part->real_mimetype) {
- $part_orig_mimetype = $mail_part->mimetype;
- $part_mimetype = $mail_part->real_mimetype;
- list($primary_type, $secondary_type) = explode('/', $part_mimetype);
- }
- else
- $part_mimetype = $mail_part->mimetype;
-
- // multipart/alternative
- if ($primary_type == 'multipart') {
+ // multipart/alternative or message/rfc822
+ if ($primary_type == 'multipart' || $part_mimetype == 'message/rfc822') {
$this->parse_structure($mail_part, true);
// list message/rfc822 as attachment as well (mostly .eml)
- if ($part_orig_mimetype == 'message/rfc822' && !empty($mail_part->filename))
+ if ($primary_type == 'message' && !empty($mail_part->filename)) {
$this->attachments[] = $mail_part;
+ }
}
// part text/[plain|html] or delivery status
else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') ||
@@ -515,8 +545,9 @@
array('object' => $this, 'structure' => $mail_part,
'mimetype' => $part_mimetype, 'recursive' => true));
- if ($plugin['abort'])
+ if ($plugin['abort']) {
continue;
+ }
if ($part_mimetype == 'text/html' && $mail_part->size) {
$got_html_part = true;
@@ -538,18 +569,6 @@
if (!empty($mail_part->filename)) {
$this->attachments[] = $mail_part;
}
- // list html part as attachment (here the part is most likely inside a multipart/related part)
- else if ($this->parse_alternative && ($secondary_type == 'html' && !$this->opt['prefer_html'])) {
- $this->attachments[] = $mail_part;
- }
- }
- // part message/*
- else if ($primary_type == 'message') {
- $this->parse_structure($mail_part, true);
-
- // list as attachment as well (mostly .eml)
- if (!empty($mail_part->filename))
- $this->attachments[] = $mail_part;
}
// ignore "virtual" protocol parts
else if ($primary_type == 'protocol') {
@@ -574,21 +593,14 @@
// part belongs to a related message and is linked
if (preg_match('/^multipart\/(related|relative)/', $mimetype)
- && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) {
+ && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])
+ ) {
if ($mail_part->headers['content-id'])
$mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']);
if ($mail_part->headers['content-location'])
$mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location'];
$this->inline_parts[] = $mail_part;
- }
- // attachment encapsulated within message/rfc822 part needs further decoding (#1486743)
- else if ($part_orig_mimetype == 'message/rfc822') {
- $this->parse_structure($mail_part, true);
-
- // list as attachment as well (mostly .eml)
- if (!empty($mail_part->filename))
- $this->attachments[] = $mail_part;
}
// regular attachment with valid content type
// (content-type name regexp according to RFC4288.4.2)
@@ -605,9 +617,13 @@
$this->attachments[] = $mail_part;
}
}
- // attachment part as message/rfc822 (#1488026)
- else if ($mail_part->mimetype == 'message/rfc822') {
- $this->parse_structure($mail_part);
+ // calendar part not marked as attachment (#1490325)
+ else if ($part_mimetype == 'text/calendar') {
+ if (!$mail_part->filename) {
+ $mail_part->filename = 'calendar.ics';
+ }
+
+ $this->attachments[] = $mail_part;
}
}
@@ -617,7 +633,7 @@
$img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/';
foreach ($this->inline_parts as $inline_object) {
- $part_url = $this->get_part_url($inline_object->mime_id, true);
+ $part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary);
if (isset($inline_object->content_id))
$a_replaces['cid:'.$inline_object->content_id] = $part_url;
if ($inline_object->content_location) {
@@ -731,34 +747,49 @@
}
$parts = array();
- // FIXME: line length is max.65?
- $uu_regexp = '/begin [0-7]{3,4} ([^\n]+)\n/s';
+
+ // uuencode regexp
+ $uu_regexp = '/^(begin [0-7]{3,4} ([^\n]+)\n)(([\x21-\x60]{0,65}\n){0,2})([\x21-\x60]{0,65}|`\nend)\s*\n/sm';
if (preg_match_all($uu_regexp, $part->body, $matches, PREG_SET_ORDER)) {
- // update message content-type
- $part->ctype_primary = 'multipart';
- $part->ctype_secondary = 'mixed';
- $part->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
$uu_endstring = "`\nend\n";
// add attachments to the structure
foreach ($matches as $pid => $att) {
- $startpos = strpos($part->body, $att[1]) + strlen($att[1]) + 1; // "\n"
- $endpos = strpos($part->body, $uu_endstring);
+ // make sure we're looking at a uuencoded file, and not a false positive
+ $uu_lines = explode("\n", $att[3]);
+ foreach ($uu_lines as $uu_line) {
+ if ( strlen($uu_line) == 0 ) {
+ continue;
+ }
+ $line_len = (ord(substr($uu_line, 0, 1)) - 32) & 0x3F;
+ $max_code_len = floor( ($line_len+2)/3 ) * 4;
+ $min_code_len = ceil( $line_len/3 * 4);
+ if ( strlen($uu_line)-1 < $min_code_len
+ or strlen($uu_line)-1 > $max_code_len )
+ {
+ // illegal uuencode, break out of 'foreach $matches' loop
+ break 2;
+ }
+ }
+
+ $startpos = strpos($part->body, $att[0]) + strlen($att[1]);
+ $endpos = strpos($part->body, $uu_endstring);
$filebody = substr($part->body, $startpos, $endpos-$startpos);
// remove attachments bodies from the message body
- $part->body = substr_replace($part->body, "", $startpos, $endpos+strlen($uu_endstring)-$startpos);
-
+ $uu_startpos = $startpos - strlen($att[1]);
+ $part->body = substr_replace($part->body, "", $uu_startpos, $endpos+strlen($uu_endstring)-$uu_startpos);
+
$uupart = new rcube_message_part;
- $uupart->filename = trim($att[1]);
+ $uupart->filename = trim($att[2]);
$uupart->encoding = 'stream';
$uupart->body = convert_uudecode($filebody);
$uupart->size = strlen($uupart->body);
$uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid;
- $ctype = rcube_mime::content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
+ $ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
$uupart->mimetype = $ctype;
list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
@@ -766,8 +797,8 @@
unset($matches[$pid]);
}
- // remove attachments bodies from the message body
- $part->body = preg_replace($uu_regexp, '', $part->body);
+ // mark body as modified so it will not be cached by rcube_imap_cache
+ $part->body_modified = true;
}
return $parts;
--
Gitblit v1.9.1