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 | 314 +++++++++++++++++++++++++++++++--------------------
1 files changed, 189 insertions(+), 125 deletions(-)
diff --git a/program/lib/Roundcube/rcube_message.php b/program/lib/Roundcube/rcube_message.php
index 87319f0..6268228 100644
--- a/program/lib/Roundcube/rcube_message.php
+++ b/program/lib/Roundcube/rcube_message.php
@@ -2,8 +2,6 @@
/*
+-----------------------------------------------------------------------+
- | program/include/rcube_message.php |
- | |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2010, The Roundcube Dev Team |
| |
@@ -18,7 +16,6 @@
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
-
/**
* Logical representation of a mail message with all its data
@@ -88,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'),
@@ -128,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);
}
@@ -147,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;
}
@@ -156,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;
}
@@ -173,10 +167,12 @@
* @param string $mime_id Part MIME-ID
* @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)
+ 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)
@@ -190,45 +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);
+ 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 level too deep or part has a file name
- if (count($level) > 2 || $part->filename) {
- 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;
}
- // HTML part can be on the lower level, if not...
- if (count($level) > 1) {
- array_pop($level);
- $parent = $this->mime_parts[join('.', $level)];
- // ... parent isn't multipart/alternative or related
- if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
- continue;
- }
+ $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;
+ }
}
}
@@ -273,7 +315,7 @@
$out = $this->get_part_content($mime_id);
// create instance of html2text class
- $txt = new html2text($out);
+ $txt = new rcube_html2text($out);
return $txt->get_text();
}
}
@@ -319,16 +361,24 @@
private function parse_structure($structure, $recursive = false)
{
// real content-type of message/rfc822 part
- if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype)
+ if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) {
$mimetype = $structure->real_mimetype;
+
+ // parse headers from message/rfc822 part
+ if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
+ list($headers, ) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 32768));
+ $structure->headers = rcube_mime::parse_headers($headers);
+ }
+ }
else
$mimetype = $structure->mimetype;
// show message headers
- if ($recursive && is_array($structure->headers) && isset($structure->headers['subject'])) {
+ 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;
+ $c->headers = $structure->headers;
$this->parts[] = $c;
}
@@ -345,45 +395,66 @@
// print body if message doesn't have multiple parts
if ($message_ctype_primary == 'text' && !$recursive) {
+ // parts with unsupported type add to attachments list
+ if (!in_array($message_ctype_secondary, array('plain', 'html', 'enriched'))) {
+ $this->attachments[] = $structure;
+ return;
+ }
+
$structure->type = 'content';
- $this->parts[] = &$structure;
+ $this->parts[] = $structure;
// Parse simple (plain text) message body
- if ($message_ctype_secondary == 'plain')
+ if ($message_ctype_secondary == 'plain') {
foreach ((array)$this->uu_decode($structure) as $uupart) {
$this->mime_parts[$uupart->mime_id] = $uupart;
$this->attachments[] = $uupart;
}
+ }
}
// the same for pgp signed messages
else if ($mimetype == 'application/pgp' && !$recursive) {
$structure->type = 'content';
- $this->parts[] = &$structure;
+ $this->parts[] = $structure;
}
// message contains (more than one!) alternative parts
else if ($mimetype == 'multipart/alternative'
&& is_array($structure->parts) && count($structure->parts) > 1
) {
- // get html/plaintext parts
- $plain_part = $html_part = $print_part = $related_part = null;
+ $plain_part = null;
+ $html_part = null;
+ $print_part = null;
+ $related_part = null;
+ $attach_part = null;
+ // get html/plaintext parts, other add to attachments list
foreach ($structure->parts as $p => $sub_part) {
$sub_mimetype = $sub_part->mimetype;
+ $is_multipart = preg_match('/^multipart\/(related|relative|mixed|alternative)/', $sub_mimetype);
// skip empty text parts
- if (!$sub_part->size && preg_match('#^text/(plain|html|enriched)$#', $sub_mimetype)) {
+ if (!$sub_part->size && !$is_multipart) {
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 ($sub_mimetype == 'text/plain')
- $plain_part = $p;
- else if ($sub_mimetype == 'text/html')
- $html_part = $p;
- else if ($sub_mimetype == 'text/enriched')
- $enriched_part = $p;
- else if (in_array($sub_mimetype, array('multipart/related', 'multipart/mixed', 'multipart/alternative')))
+ if ($is_multipart)
$related_part = $p;
+ else if ($sub_mimetype == 'text/plain' && !$plain_part)
+ $plain_part = $p;
+ else if ($sub_mimetype == 'text/html' && !$html_part)
+ $html_part = $p;
+ else if ($sub_mimetype == 'text/enriched' && !$enriched_part)
+ $enriched_part = $p;
+ else {
+ // add unsupported/unrecognized parts to attachments list
+ $this->attachments[] = $sub_part;
+ }
}
// parse related part (alternative part could be in here)
@@ -399,13 +470,13 @@
// choose html/plain part to print
if ($html_part !== null && $this->opt['prefer_html']) {
- $print_part = &$structure->parts[$html_part];
+ $print_part = $structure->parts[$html_part];
}
else if ($enriched_part !== null) {
- $print_part = &$structure->parts[$enriched_part];
+ $print_part = $structure->parts[$enriched_part];
}
else if ($plain_part !== null) {
- $print_part = &$structure->parts[$plain_part];
+ $print_part = $structure->parts[$plain_part];
}
// add the right message body
@@ -424,14 +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;
- }
}
// this is an ecrypted message -> create a plaintext body with the according message
else if ($mimetype == 'multipart/encrypted') {
@@ -444,6 +507,17 @@
$this->parts[] = $p;
}
+ // this is an S/MIME ecrypted message -> create a plaintext body with the according message
+ else if ($mimetype == 'application/pkcs7-mime') {
+ $p = new stdClass;
+ $p->type = 'content';
+ $p->ctype_primary = 'text';
+ $p->ctype_secondary = 'plain';
+ $p->mimetype = 'text/plain';
+ $p->realtype = 'application/pkcs7-mime';
+
+ $this->parts[] = $p;
+ }
// message contains multiple parts
else if (is_array($structure->parts) && !empty($structure->parts)) {
// iterate over parts
@@ -451,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') ||
@@ -478,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;
@@ -501,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') {
@@ -536,8 +592,9 @@
continue;
// part belongs to a related message and is linked
- if ($mimetype == 'multipart/related'
- && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) {
+ if (preg_match('/^multipart\/(related|relative)/', $mimetype)
+ && ($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'])
@@ -545,20 +602,9 @@
$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)
else if (preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) {
- if (!$mail_part->filename)
- $mail_part->filename = 'Part '.$mail_part->mime_id;
-
$this->attachments[] = $mail_part;
}
// attachment with invalid content type
@@ -571,20 +617,24 @@
$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;
}
}
// if this was a related part try to resolve references
- if ($mimetype == 'multipart/related' && sizeof($this->inline_parts)) {
+ if (preg_match('/^multipart\/(related|relative)/', $mimetype) && sizeof($this->inline_parts)) {
$a_replaces = array();
$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);
- if ($inline_object->content_id)
+ $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) {
$a_replaces[$inline_object->content_location] = $part_url;
@@ -623,7 +673,6 @@
}
// message is a single part non-text (without filename)
else if (preg_match('/application\//i', $mimetype)) {
- $structure->filename = 'Part '.$structure->mime_id;
$this->attachments[] = $structure;
}
}
@@ -698,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);
@@ -733,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