Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render each mime-part into an individual iframe #9519

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
- Fix Oauth issues with use_secure_urls=true (#9722)
- Fix handling of binary mail parts (e.g. PDF) encoded with quoted-printable (#9728)
- Clear "list is empty" message on loading a new list. Previously that message was still visible until the new list was fully loaded, which (if loading was slow) could give the impression that the newly loading is list empty, too. (#9006)
- Render all email content parts in an individual iframe to mitigate scripting, redressing, and other attacks. (#9519)

## Release 1.6.9

Expand Down
87 changes: 47 additions & 40 deletions plugins/hide_blockquote/hide_blockquote.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,64 @@

if (window.rcmail) {
rcmail.addEventListener('init', function () {
hide_blockquote();
});
}
var limit = rcmail.env.blockquote_limit;

function hide_blockquote() {
var limit = rcmail.env.blockquote_limit;
if (limit <= 0) {
return;
}

if (limit <= 0) {
return;
}
$('.framed-message-part').each(function (_id, iframe) {
$(iframe).on('load', function () {
$(iframe.contentDocument).find('.message-part div.pre > blockquote').each(function (_id, elem) {
hide_blockquote(elem, limit);
});
window.dispatchEvent(new Event('resize'));
});
});
});
}

$('div.message-part div.pre > blockquote', $('#messagebody')).each(function () {
var res, text, div, link, q = $(this);
function hide_blockquote(elem, limit) {
var res, text, div, link, q = $(elem);

// Add new-line character before each blockquote
// This fixes counting lines of text, it also prevents
// from merging lines from different quoting level
$('blockquote').before(document.createTextNode('\n'));
// Add new-line character before each blockquote
// This fixes counting lines of text, it also prevents
// from merging lines from different quoting level
$('blockquote').before(document.createTextNode('\n'));

text = q.text().trim();
res = text.split(/\n/);
text = q.text().trim();
res = text.split(/\n/);

if (res.length <= limit) {
// there can be also a block with very long wrapped line
// assume line height = 15px
if (q.height() <= limit * 15) {
return;
}
if (res.length <= limit) {
// there can be also a block with very long wrapped line
// assume line height = 15px
if (q.height() <= limit * 15) {
return;
}
}

div = $('<blockquote class="blockquote-header">')
.css({ 'white-space': 'nowrap', overflow: 'hidden', position: 'relative' })
.text(res[0]);
div = $('<blockquote class="blockquote-header">')
.css({ 'white-space': 'nowrap', overflow: 'hidden', position: 'relative' })
.text(res[0]);

link = $('<span class="blockquote-link"></span>')
.css({ position: 'absolute', 'z-Index': 2 })
.text(rcmail.get_label('hide_blockquote.show'))
.data('parent', div)
.click(function () {
var t = $(this), parent = t.data('parent'), visible = parent.is(':visible');
link = $('<span class="blockquote-link"></span>')
.css({ position: 'absolute', 'z-Index': 2 })
.text(rcmail.get_label('hide_blockquote.show'))
.data('parent', div)
.click(function () {
var t = $(this), parent = t.data('parent'), visible = parent.is(':visible');

t.text(rcmail.get_label(visible ? 'hide' : 'show', 'hide_blockquote'))
.detach().appendTo(visible ? q : parent).toggleClass('collapsed');
t.text(rcmail.get_label(visible ? 'hide' : 'show', 'hide_blockquote'))
.detach().appendTo(visible ? q : parent).toggleClass('collapsed');

parent[visible ? 'hide' : 'show']();
q[visible ? 'show' : 'hide']();
});
parent[visible ? 'hide' : 'show']();
q[visible ? 'show' : 'hide']();

link.appendTo(div);
window.dispatchEvent(new Event('resize'));
});

// Modify blockquote
q.hide().css({ position: 'relative' }).before(div);
});
link.appendTo(div);

// Modify blockquote
q.hide().css({ position: 'relative' }).before(div);
}
120 changes: 73 additions & 47 deletions program/actions/mail/get.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,6 @@ public function run($args = [])
// This resets X-Frame-Options for framed output (#6688)
$rcmail->output->page_headers();

// show loading page
if (!empty($_GET['_preload'])) {
unset($_GET['_preload']);
unset($_GET['_safe']);

$url = $rcmail->url($_GET + ['_mimewarning' => 1, '_embed' => 1]);
$message = $rcmail->gettext('loadingdata');

header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
echo "<html>\n<head>\n"
. '<meta http-equiv="refresh" content="0; url=' . rcube::Q($url) . '">' . "\n"
. '<meta http-equiv="content-type" content="text/html; charset=' . RCUBE_CHARSET . '">' . "\n"
. "</head>\n<body>\n{$message}\n</body>\n</html>";
exit;
}

$attachment = new rcmail_attachment_handler();
$mimetype = $attachment->mimetype;
$filename = $attachment->filename;
Expand Down Expand Up @@ -117,7 +101,7 @@ public function run($args = [])
readfile($cache_file);
}

exit;
$rcmail->output->sendExit();
}
}

Expand Down Expand Up @@ -197,8 +181,7 @@ public function run($args = [])
}
// html warning with a button to load the file anyway
else {
$rcmail->output = new rcmail_html_page();
$rcmail->output->register_inline_warning(
$inline_warning = $this->make_inline_warning(
$rcmail->gettext([
'name' => 'attachmentvalidationerror',
'vars' => [
Expand All @@ -209,11 +192,10 @@ public function run($args = [])
$rcmail->gettext('showanyway'),
$rcmail->url(array_merge($_GET, ['_nocheck' => 1]))
);

$rcmail->output->write();
$this->send_html('', $inline_warning);
}

exit;
$rcmail->output->sendExit();
}
}

Expand All @@ -232,40 +214,36 @@ public function run($args = [])
}
}

// Deliver plaintext with HTML-markup
if ($mimetype == 'text/plain' && empty($_GET['_download'])) {
$body = $attachment->print_body();
// Don't use rcmail_html_page here, because that always loads
// embed.css and blocks loading other css files (though calling
// reset() in write()). Also we don't need all the processing that it
// brings.
$styles_path = $rcmail->output->get_skin_file('/styles/styles.css', $path, null, true);
$body = html::tag('html', [],
html::tag('head', [], html::tag('link', ['rel' => 'stylesheet', 'href' => $styles_path]))
. html::tag('body', [], $body)
);
$rcmail->output->sendExit($body);
}

// deliver part content
if ($mimetype == 'text/html' && empty($_GET['_download'])) {
$rcmail->output = new rcmail_html_page();
$out = '';

// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (!rcube_utils::mem_check($attachment->size * 10)) {
$rcmail->output->register_inline_warning(
$inline_warning = $this->make_inline_warning(
$rcmail->gettext('messagetoobig'),
$rcmail->gettext('download'),
$rcmail->url(array_merge($_GET, ['_download' => 1]))
);
$this->send_html('', $inline_warning);
} else {
// render HTML body
$out = $attachment->html();

// insert remote objects warning into HTML body
if (self::$REMOTE_OBJECTS) {
$rcmail->output->register_inline_warning(
$rcmail->gettext('blockedresources'),
$rcmail->gettext('allow'),
$rcmail->url(array_merge($_GET, ['_safe' => 1]))
);
} else {
// Use strict security policy to make sure no javascript is executed
// TODO: Make the above "blocked resources button" working with strict policy
// TODO: Move this to rcmail_html_page::write()?
header("Content-Security-Policy: script-src 'none'");
}
$this->send_html($attachment->html());
}

$rcmail->output->write($out);
exit;
}

// add filename extension if missing
Expand Down Expand Up @@ -295,12 +273,12 @@ public function run($args = [])
$attachment->output($mimetype);
}

exit;
$rcmail->output->sendExit();
}

// if we arrive here, the requested part was not found
http_response_code(404);
exit;
$rcmail->output->sendExit();
}

/**
Expand Down Expand Up @@ -357,16 +335,64 @@ public static function message_part_frame($attrib)
} else {
$mimetype = $rcmail->output->get_env('mimetype');
$url = $_GET;
$url[strpos($mimetype, 'text/') === 0 ? '_embed' : '_preload'] = 1;
$url['_embed'] = 1;
unset($url['_frame']);
}

$url['_framed'] = 1; // For proper X-Frame-Options:deny handling

$attrib['src'] = $rcmail->url($url);
$attrib['sandbox'] = 'allow-same-origin';

$rcmail->output->add_gui_object('messagepartframe', $attrib['id']);

return html::iframe($attrib);
}

protected function send_html($contents, $inline_warning = null)
{
$rcmail = rcmail::get_instance();
$rcmail->output->reset(true);

// load embed.css from skin folder (if exists)
$embed_css = $rcmail->config->get('embed_css_location', '/embed.css');
if ($embed_css = $rcmail->output->get_skin_file($embed_css, $path, null, true)) {
$rcmail->output->include_css($embed_css);
} else { // set default styles for warning blocks inside the attachment part frame
$rcmail->output->add_header(html::tag('style', ['type' => 'text/css'],
'.rcmail-inline-message { font-family: sans-serif; border:2px solid #ffdf0e;'
. "background:#fef893; padding:0.6em 1em; margin-bottom:0.6em }\n" .
'.rcmail-inline-buttons { margin-bottom:0 }'
));
}

if (empty($contents)) {
$contents = '<html><body></body></html>';
}

if ($inline_warning) {
$body_start = 0;
if ($body_pos = strpos($contents, '<body')) {
$body_start = strpos($contents, '>', $body_pos) + 1;
}

$contents = substr_replace($contents, $inline_warning, $body_start, 0);
}

$rcmail->output->write($contents);
$rcmail->output->sendExit();
}

protected function make_inline_warning($text, $button_label = null, $button_url = null)
{
$text = html::span(null, $text);

if ($button_label) {
$onclick = "location.href = '{$button_url}'";
$button = html::tag('button', ['onclick' => $onclick], rcube::Q($button_label));
$text .= html::p(['class' => 'rcmail-inline-buttons'], $button);
}

return html::div(['class' => 'rcmail-inline-message rcmail-inline-warning'], $text);
}
}
20 changes: 18 additions & 2 deletions program/actions/mail/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class rcmail_action_mail_index extends rcmail_action
];

protected static $PRINT_MODE = false;
protected static $REMOTE_OBJECTS;
protected static $SUSPICIOUS_EMAIL = false;
protected static $wash_html_body_attrs = [];

Expand Down Expand Up @@ -157,6 +156,7 @@ public function run($args = [])
'searchfilter' => [$this, 'search_filter'],
'searchinterval' => [$this, 'search_interval'],
'searchform' => [$rcmail->output, 'search_form'],
'messageloadingnotice' => [$this, 'message_loading_notice'],
]);
}

Expand Down Expand Up @@ -1003,7 +1003,14 @@ public static function wash_html($html, $p, $cid_replaces = [])
$html = rcube_charset::clean($html);

$html = $washer->wash($html);
self::$REMOTE_OBJECTS = $washer->extlinks;
if ($washer->extlinks) {
// This is an ugly solution, but the least invasive I could think
// of. The problem is that the "washer" traverses the node tree
// from the top and produces a string containing HTML code - so
// after "washiing" we have only a big string, and before "washing"
// we don't yet know if any remote references are present.
$html = str_replace('<body ', '<body data-extlinks="true" ', $html);
}

// There was no <body>, but a wrapper element is required
if (!empty($p['inline_html']) && !empty(self::$wash_html_body_attrs)) {
Expand Down Expand Up @@ -1668,4 +1675,13 @@ public static function supported_mimetypes()

return array_values($mimetypes);
}

public static function message_loading_notice()
{
$rcmail = rcmail::get_instance();
return html::div(['class' => 'iframe-loading-message ui alert loading'], [
html::tag('i', ['class' => 'icon'], ''),
html::span([], $rcmail->gettext('loadingdata')),
]);
}
}
Loading
Loading