From c14b337450bb546f5c1b18b1a66481844a3e79d0 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Tue, 27 Nov 2012 16:25:42 +0100 Subject: Fix XSS vulnerability using Flash files (#1488828) by comparing mimetypes and filename extensions --- program/localization/en_US/labels.inc | 1 + program/localization/en_US/messages.inc | 1 + program/steps/mail/func.inc | 2 +- program/steps/mail/get.inc | 72 +++++++++++++++++++++++++++++++-- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index c8cbf1841..abb0dca5d 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -64,6 +64,7 @@ $labels['move'] = 'Move'; $labels['moveto'] = 'Move to...'; $labels['download'] = 'Download'; $labels['showattachment'] = 'Show'; +$labels['showanyway'] = 'Show it anyway'; $labels['filename'] = 'File name'; $labels['filesize'] = 'File size'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index a900fae18..68cf314e7 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -163,6 +163,7 @@ $messages['invalidimageformat'] = 'Not a valid image format.'; $messages['mispellingsfound'] = 'Spelling errors detected in the message.'; $messages['parentnotwritable'] = 'Unable to create/move folder into selected parent folder. No access rights.'; $messages['messagetoobig'] = 'The message part is too big to process it.'; +$messages['attachmentvalidationerror'] = 'WARNING! This attachment is suspicious because its type doesn\'t match the type declared in the message. If you do not trust the sender, you shouldn\'t open it in the browser because it may contain malicious contents.

Expected: $expected; found: $detected'; $messages['noscriptwarning'] = 'Warning: This webmail service requires Javascript! In order to use it please enable Javascript in your browser\'s settings.'; ?> diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index ff442ad60..cb1a5ddae 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -1664,7 +1664,7 @@ function rcmail_message_part_frame($attrib) $part = $MESSAGE->mime_parts[asciiwords(get_input_value('_part', RCUBE_INPUT_GPC))]; $ctype_primary = strtolower($part->ctype_primary); - $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary=='text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']); + $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary=='text' ? '_embed=' : '_preload='), $_SERVER['QUERY_STRING']); return html::iframe($attrib); } diff --git a/program/steps/mail/get.inc b/program/steps/mail/get.inc index 2397358a1..71a5e1b02 100644 --- a/program/steps/mail/get.inc +++ b/program/steps/mail/get.inc @@ -112,6 +112,71 @@ else if (strlen($pid = get_input_value('_part', RCUBE_INPUT_GET))) { // overwrite modified vars from plugin $mimetype = $plugin['mimetype']; + $extensions = rcube_mime::get_mime_extensions($mimetype); + + if ($plugin['body']) + $part->body = $plugin['body']; + + + // compare file mimetype with the stated content-type headers and file extension to avoid malicious operations + if (!empty($_REQUEST['_embed']) && empty($_REQUEST['_nocheck'])) { + $file_extension = strtolower(pathinfo($part->filename, PATHINFO_EXTENSION)); + + // 1. compare filename suffix with expected suffix derived from mimetype + $valid = $file_extension && in_array($file_extension, (array)$extensions); + + // 2. detect the real mimetype of the attachment part and compare it with the stated mimetype and filename extension + if ($valid || !$file_extension || $mimetype == 'application/octet-stream') { + if ($part->body) // part body is already loaded + $body = $part->body; + else if ($part->size && $part->size < 1024*1024) // load the entire part if it's small enough + $body = $part->body = $MESSAGE->get_part_content($part->mime_id); + else // fetch the first 2K of the message part + $body = $MESSAGE->get_part_content($part->mime_id, null, true, 2048); + + // detect message part mimetype + $real_mimetype = rcube_mime::file_content_type($body, $part->filename, $mimetype, true, true); + list($real_ctype_primary, $real_ctype_secondary) = explode('/', $real_mimetype); + + // ignore differences in text/* mimetypes. Filetype detection isn't very reliable here + if ($real_ctype_primary == 'text' && strpos($mimetype, $real_ctype_primary) === 0) + $real_mimetype = $mimetype; + + // get valid file extensions + $extensions = rcube_mime::get_mime_extensions($real_mimetype); + $valid_extension = (!$file_extension || in_array($file_extension, (array)$extensions)); + + // fix mimetype for images wrongly declared as octet-stream + if ($mimetype == 'application/octet-stream' && strpos($real_mimetype, 'image/') === 0 && $valid_extension) + $mimetype = $real_mimetype; + + $valid = ($real_mimetype == $mimetype && $valid_extension); + } + else { + $real_mimetype = $mimetype; + } + + // show warning if validity checks failed + if (!$valid) { + $OUTPUT = new rcmail_html_page(); + $OUTPUT->write(html::tag('html', null, html::tag('body', array('style' => 'font-family:sans-serif; margin:1em'), + html::div(array('class' => 'warning', 'style' => 'border:2px solid #ffdf0e; background:#fef893; padding:1em 1em 0 1em;'), + rcube_label(array( + 'name' => 'attachmentvalidationerror', + 'vars' => array('expected' => "$mimetype (.$file_extension)", 'detected' => "$real_mimetype (.$extensions[0])") + )) . + html::p('buttons', + html::tag('button', null, + html::a(array( + 'href' => $RCMAIL->url(array_merge($_GET, array('_nocheck' => 1))), + 'style' => 'text-decoration:none;color:#000', + ), rcube_label('showanyway'))) + )) + ))); + exit; + } + } + // TIFF to JPEG conversion, if needed $tiff_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['tif']); @@ -123,11 +188,9 @@ else if (strlen($pid = get_input_value('_part', RCUBE_INPUT_GET))) { $mimetype = 'image/jpeg'; } - list($ctype_primary, $ctype_secondary) = explode('/', $mimetype); - if ($plugin['body']) - $part->body = $plugin['body']; $browser = $RCMAIL->output->browser; + list($ctype_primary, $ctype_secondary) = explode('/', $mimetype); // send download headers if ($plugin['download']) { @@ -144,6 +207,7 @@ else if (strlen($pid = get_input_value('_part', RCUBE_INPUT_GET))) { header("Content-Transfer-Encoding: binary"); } + // deliver part content if ($ctype_primary == 'text' && $ctype_secondary == 'html' && empty($plugin['download'])) { // Check if we have enough memory to handle the message in it @@ -166,7 +230,7 @@ else if (strlen($pid = get_input_value('_part', RCUBE_INPUT_GET))) { check_storage_status(); } - $OUTPUT = new rcube_output_html(); + $OUTPUT = new rcube_html_page(); $OUTPUT->write($out); } else { -- cgit v1.2.3