[Pkg-roundcube-maintainers] CVE-2024-42009/roundcube: bullseye-security upload

Guilhem Moulin guilhem at debian.org
Mon Aug 12 12:05:09 BST 2024


Hi,

On Tue, 06 Aug 2024 at 18:31:37 +0200, Guilhem Moulin wrote:
> Unfortunately for bullseye the backport requires more work.  I'll try to
> do that ASAP but I'm not yet sure that I will have time to finalize it
> before the suite is handed over to the LTS team.

Thanks for the bookworm DSA!  Here comes a debdiff for bullseye-security.
The backports were quite intrusive but have now been thoroughly tested,
incl. on two production instances totaling ~200 users AFAIK none of whom
didn't report any misbehavior.

Note that the original patch for CVE-2024-42008 introduces a regression
(#1078456): it sets a too restrictive Content-Security-Policy on the
attachment preview page which breaks printing and other handling of
image attachments.  I backported the fix for 1.4.15+dfsg.1-1+deb11u4,
but 1.6.5+dfsg-1+deb12u3 is affected.  I assume this doesn't warrant a
follow-up DSA, right?  Will go via s-pu in that case.

Cheers,
-- 
Guilhem.
-------------- next part --------------
diffstat for roundcube-1.4.15+dfsg.1 roundcube-1.4.15+dfsg.1

 changelog                                                               |   13 
 patches/CVE-2024-42008.patch                                            |   71 +
 patches/CVE-2024-42009.patch                                            |  616 ++++++++++
 patches/CVE-2024-42010.patch                                            |  611 +++++++++
 patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch     |   52 
 patches/Fix-regression-where-printing-scaling-rotating-image-atta.patch |   71 +
 patches/series                                                          |    5 
 7 files changed, 1439 insertions(+)

diff -Nru roundcube-1.4.15+dfsg.1/debian/changelog roundcube-1.4.15+dfsg.1/debian/changelog
--- roundcube-1.4.15+dfsg.1/debian/changelog	2024-06-17 04:10:38.000000000 +0200
+++ roundcube-1.4.15+dfsg.1/debian/changelog	2024-08-08 23:48:56.000000000 +0200
@@ -1,3 +1,16 @@
+roundcube (1.4.15+dfsg.1-1+deb11u4) bullseye-security; urgency=high
+
+  * Fix CVE-2024-42008: Cross-site scripting (XSS) vulnerability in serving of
+    attachments other than HTML or SVG.
+  * Fix CVE-2024-42009: Cross-site scripting (XSS) vulnerability in
+    post-processing of sanitized HTML content. (Closes: #1077969)
+  * Fix CVE-2024-42010: Information leak (access to remote content) via
+    insufficient CSS filtering.
+  * Backport upstream fix for infinite loop when parsing malformed Sieve
+    script.
+
+ -- Guilhem Moulin <guilhem at debian.org>  Thu, 08 Aug 2024 23:48:56 +0200
+
 roundcube (1.4.15+dfsg.1-1+deb11u3) bullseye-security; urgency=high
 
   * Fix CVE-2024-37384: Cross-site scripting (XSS) vulnerability in handling
diff -Nru roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42008.patch roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42008.patch
--- roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42008.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42008.patch	2024-08-08 23:48:56.000000000 +0200
@@ -0,0 +1,71 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Sat, 3 Aug 2024 09:10:55 +0200
+Subject: Fix XSS vulnerability in serving of attachments other than HTML or
+ SVG
+
+Credits to Oskar Zeino-Mahmalat (Sonar) https://www.sonarsource.com
+
+Origin: https://github.com/roundcube/roundcubemail/commit/c222ea8b99448ead20ab3864fcc29c84ed17403a
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-42008
+Bug-Debian: https://bugs.debian.org/1077969
+---
+ program/lib/Roundcube/rcube_output.php | 20 +++++++++++++++-----
+ program/steps/mail/get.inc             |  5 +++++
+ 2 files changed, 20 insertions(+), 5 deletions(-)
+
+diff --git a/program/lib/Roundcube/rcube_output.php b/program/lib/Roundcube/rcube_output.php
+index f24c47f..a51b75b 100644
+--- a/program/lib/Roundcube/rcube_output.php
++++ b/program/lib/Roundcube/rcube_output.php
+@@ -242,13 +242,20 @@ abstract class rcube_output
+             $ctype = $params['type'];
+         }
+ 
+-        if ($disposition == 'inline' && stripos($ctype, 'text') === 0) {
+-            $charset = $this->charset;
+-            if (!empty($params['type_charset']) && rcube_charset::is_valid($params['type_charset'])) {
+-                $charset = $params['type_charset'];
++        // Send unsafe content as plain text
++        if ($disposition == 'inline') {
++            if (preg_match('~(javascript|jscript|ecmascript|xml|html|text/)~i', $ctype)) {
++                $ctype = 'text/plain';
+             }
+ 
+-            $ctype .= "; charset={$charset}";
++            if (stripos($ctype, 'text') === 0) {
++                $charset = $this->charset;
++                if (!empty($params['type_charset']) && rcube_charset::is_valid($params['type_charset'])) {
++                    $charset = $params['type_charset'];
++                }
++
++                $ctype .= "; charset={$charset}";
++            }
+         }
+ 
+         if (is_string($filename) && strlen($filename) > 0 && strlen($filename) <= 1024) {
+@@ -278,6 +285,9 @@ abstract class rcube_output
+             header("Content-Length: " . $params['length']);
+         }
+ 
++        // Use strict security policy to make sure no javascript content is executed
++        header("Content-Security-Policy: default-src 'none'");
++
+         // don't kill the connection if download takes more than 30 sec.
+         if (!array_key_exists('time_limit', $params)) {
+             $params['time_limit'] = 3600;
+diff --git a/program/steps/mail/get.inc b/program/steps/mail/get.inc
+index 8e3647f..db152cb 100644
+--- a/program/steps/mail/get.inc
++++ b/program/steps/mail/get.inc
+@@ -240,6 +240,11 @@ if (empty($_GET['_thumb']) && $attachment->is_valid()) {
+                     $RCMAIL->gettext('allow'),
+                     $RCMAIL->url(array_merge($_GET, array('_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'");
+             }
+         }
+ 
diff -Nru roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42009.patch roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42009.patch
--- roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42009.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42009.patch	2024-08-08 23:48:56.000000000 +0200
@@ -0,0 +1,616 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Sat, 3 Aug 2024 09:09:09 +0200
+Subject: Fix XSS vulnerability in post-processing of sanitized HTML content
+
+Credits to Oskar Zeino-Mahmalat (https://www.sonarsource.com)
+
+Origin: https://github.com/roundcube/roundcubemail/commit/1b3bb11d4f3880ba2ac8d1f398d32bf99be47122
+Origin: https://github.com/roundcube/roundcubemail/commit/a25e48e2daec522432fea3c37f3917366e2948d1
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-42009
+Bug-Debian: https://bugs.debian.org/1077969
+---
+ program/lib/Roundcube/rcube_washtml.php  |  46 +++++---
+ program/steps/mail/compose.inc           |  37 ++----
+ program/steps/mail/func.inc              | 189 +++++++++++++------------------
+ program/steps/mail/show.inc              |   4 -
+ program/steps/settings/save_identity.inc |   9 +-
+ tests/MailFunc.php                       |  69 +++++++----
+ 6 files changed, 170 insertions(+), 184 deletions(-)
+
+diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
+index 8cb0d24..8a3a5ab 100644
+--- a/program/lib/Roundcube/rcube_washtml.php
++++ b/program/lib/Roundcube/rcube_washtml.php
+@@ -164,7 +164,14 @@ class rcube_washtml
+     public $extlinks = false;
+ 
+     /** @var array Current settings */
+-    private $config = array();
++    private $config = array(
++        'add_comments' => true,
++        'allow_remote' => false,
++        'base_url' => '',
++        'charset' => RCUBE_CHARSET,
++        'cid_map' => array(),
++        'show_washed' => true,
++    );
+ 
+     /** @var array Registered callback functions for tags */
+     private $handlers = array();
+@@ -217,7 +224,7 @@ class rcube_washtml
+ 
+         unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements'], $p['css_prefix']);
+ 
+-        $this->config = $p + array('show_washed' => true, 'allow_remote' => false, 'cid_map' => array());
++        $this->config = array_merge($this->config, $p);
+     }
+ 
+     /**
+@@ -562,7 +569,7 @@ class rcube_washtml
+                     true, false);
+             }
+ 
+-            return '<!-- ignored -->';
++            return $this->config['add_comments'] ? '<!-- ignored -->' : '';
+         }
+ 
+         $node = $node->firstChild;
+@@ -576,7 +583,9 @@ class rcube_washtml
+                 if ($tagName == 'link') {
+                     $uri = $this->wash_uri($node->getAttribute('href'), false, false);
+                     if (!$uri) {
+-                        $dump .= '<!-- link ignored -->';
++                        if ($this->config['add_comments']) {
++                            $dump .= '<!-- link ignored -->';
++                        }
+                         break;
+                     }
+ 
+@@ -586,7 +595,9 @@ class rcube_washtml
+                     && self::attribute_value($node, 'attributename', 'href')
+                 ) {
+                     // Insecure svg tags
+-                    $dump .= "<!-- $tagName blocked -->";
++                    if ($this->config['add_comments']) {
++                        $dump .= "<!-- {$tagName} blocked -->";
++                    }
+                     break;
+                 }
+ 
+@@ -596,13 +607,13 @@ class rcube_washtml
+                 }
+                 else if (isset($this->_html_elements[$tagName])) {
+                     $content = $this->dumpHtml($node, $level);
+-                    $dump .= '<' . $node->nodeName;
++                    $tag = '<' . $node->nodeName;
+ 
+                     if ($tagName == 'svg') {
+                         $xpath = new DOMXPath($node->ownerDocument);
+                         foreach ($xpath->query('namespace::*') as $ns) {
+                             if ($ns->nodeName != 'xmlns:xml') {
+-                                $dump .= sprintf(' %s="%s"',
++                                $tag .= sprintf(' %s="%s"',
+                                     $ns->nodeName,
+                                     htmlspecialchars($ns->nodeValue, ENT_QUOTES, $this->config['charset'])
+                                 );
+@@ -613,20 +624,25 @@ class rcube_washtml
+                         $content = htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, $this->config['charset']);
+                     }
+ 
+-                    $dump .= $this->wash_attribs($node);
++                    $tag .= $this->wash_attribs($node);
+ 
+-                    if ($content === '' && ($this->is_xml || isset($this->_void_elements[$tagName]))) {
+-                        $dump .= ' />';
+-                    }
+-                    else {
+-                        $dump .= '>' . $content . '</' . $node->nodeName . '>';
++                    if (isset($this->_ignore_elements[$tagName])) {
++                        $dump .= $content;
++                    } elseif ($content === '' && ($this->is_xml || isset($this->_void_elements[$tagName]))) {
++                         $dump .= $tag . ' />';
++                    } else {
++                         $dump .= $tag . '>' . $content . '</' . $node->nodeName . '>';
+                     }
+                 }
+                 else if (isset($this->_ignore_elements[$tagName])) {
+-                    $dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' not allowed -->';
++                    if ($this->config['add_comments']) {
++                        $dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' not allowed -->';
++                    }
+                 }
+                 else {
+-                    $dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' ignored -->';
++                    if ($this->config['add_comments']) {
++                        $dump .= '<!-- ' . htmlspecialchars($node->nodeName, ENT_QUOTES, $this->config['charset']) . ' ignored -->';
++                    }
+                     $dump .= $this->dumpHtml($node, $level); // ignore tags not its content
+                 }
+                 break;
+diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
+index 73be67b..06db2b4 100644
+--- a/program/steps/mail/compose.inc
++++ b/program/steps/mail/compose.inc
+@@ -335,7 +335,7 @@ function rcmail_process_compose_params(&$COMPOSE)
+     // clean HTML message body which can be submitted by URL
+     if (!empty($COMPOSE['param']['body'])) {
+         if ($COMPOSE['param']['html'] = strpos($COMPOSE['param']['body'], '<') !== false) {
+-            $wash_params              = array('safe' => false, 'inline_html' => true);
++            $wash_params              = array('safe' => false);
+             $COMPOSE['param']['body'] = rcmail_prepare_html_body($COMPOSE['param']['body'], $wash_params);
+         }
+     }
+@@ -880,40 +880,25 @@ function rcmail_prepare_html_body($body, $wash_params = array())
+     static $part_no;
+ 
+     // Set attributes of the part container
+-    $container_id     = $COMPOSE['mode'] . 'body' . (++$part_no);
+-    $container_attrib = array('id' => $container_id);
+-
+-    $body_args = array(
+-        'safe'             => $MESSAGE->is_safe,
+-        'plain'            => false,
+-        'css_prefix'       => 'v' . $part_no,
++    $container_id = $COMPOSE['mode'] . 'body' . (++$part_no);
++    $wash_params += array(
++        'safe'         => $MESSAGE->is_safe,
++        'css_prefix'   => 'v' . $part_no,
++        'add_comments' => false,
+     );
+ 
+-    // remove comments (produced by washtml)
+-    $replace = array('/<!--[^>]+-->/' => '');
+-
+     if ($COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT) {
+         // convert TinyMCE's empty-line sequence (#1490463)
+-        $replace['/<p>\xC2\xA0<\/p>/'] = '<p><br /></p>';
+-        // remove <body> tags
+-        $replace['/<body([^>]*)>/i'] = '';
+-        $replace['/<\/body>/i']      = '';
++        $body = preg_replace('/<p>\xC2\xA0<\/p>/', '<p><br /></p>', $body);
++        // remove <body> tags (not their content)
++        $wash_params['ignore_elements'] = array('body');
+     }
+     else {
+-        $body_args['container_id']     = $container_id;
+-        $body_args['container_attrib'] = $container_attrib;
++        $wash_params['container_id'] = $container_id;
+     }
+ 
+     // Make the HTML content safe and clean
+-    $body = rcmail_wash_html($body, $wash_params + $body_args, $CID_MAP);
+-    $body = preg_replace(array_keys($replace), array_values($replace), $body);
+-    $body = rcmail_html4inline($body, $body_args);
+-
+-    if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT) {
+-        $body =  html::div($container_attrib, $body);
+-    }
+-
+-    return $body;
++    return rcmail_wash_html($body, $wash_params, $CID_MAP);
+ }
+ 
+ // Removes signature from the message body
+diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
+index 4b96270..f8aed12 100644
+--- a/program/steps/mail/func.inc
++++ b/program/steps/mail/func.inc
+@@ -836,6 +836,7 @@ function rcmail_check_safe($message)
+ function rcmail_wash_html($html, $p, $cid_replaces = array())
+ {
+     global $REMOTE_OBJECTS, $RCMAIL;
++    global $wash_html_body_attrs;
+ 
+     $p += array('safe' => false, 'inline_html' => true);
+ 
+@@ -862,13 +863,17 @@ function rcmail_wash_html($html, $p, $cid_replaces = array())
+     // clean HTML with washhtml by Frederic Motte
+     $wash_opts = array(
+         'show_washed'   => false,
++        'add_comments'  => isset($p['add_comments']) ? $p['add_comments'] : true,
+         'allow_remote'  => $p['safe'],
+         'blocked_src'   => $RCMAIL->output->asset_url('program/resources/blocked.gif'),
+         'charset'       => RCUBE_CHARSET,
+         'cid_map'       => $cid_replaces,
+         'html_elements' => array('body'),
+         'css_prefix'    => $p['css_prefix'],
++        'ignore_elements' => isset($p['ignore_elements']) ? $p['ignore_elements'] : array(),
++        // internal configuration
+         'container_id'  => $p['container_id'],
++        'body_class'    => isset($p['body_class']) ? $p['body_class'] : '',
+     );
+ 
+     if (!$p['inline_html']) {
+@@ -889,6 +894,20 @@ function rcmail_wash_html($html, $p, $cid_replaces = array())
+     // initialize HTML washer
+     $washer = new rcube_washtml($wash_opts);
+ 
++    $wash_html_body_attrs = array();
++
++    if ($p['inline_html']) {
++        $washer->add_callback('body', 'rcmail_washtml_callback');
++
++        if ($wash_opts['body_class']) {
++            $wash_html_body_attrs['class'] = $wash_opts['body_class'];
++        }
++
++        if ($wash_opts['container_id']) {
++            $wash_html_body_attrs['id'] = $wash_opts['container_id'];
++        }
++    }
++
+     if (!$p['skip_washer_form_callback']) {
+         $washer->add_callback('form', 'rcmail_washtml_callback');
+     }
+@@ -911,6 +930,11 @@ function rcmail_wash_html($html, $p, $cid_replaces = array())
+     $html = $washer->wash($html);
+     $REMOTE_OBJECTS = $washer->extlinks;
+ 
++    // There was no <body>, but a wrapper element is required
++    if ($p['inline_html'] && !empty($wash_html_body_attrs)) {
++        $html = html::tag('div', $wash_html_body_attrs, $html);
++    }
++
+     return $html;
+ }
+ 
+@@ -999,6 +1023,7 @@ function rcmail_plain_body($body, $flowed = false, $delsp = false)
+  */
+ function rcmail_washtml_callback($tagname, $attrib, $content, $washtml)
+ {
++    global $wash_html_body_attrs;
+     switch ($tagname) {
+     case 'form':
+         $out = html::div('form', $content);
+@@ -1022,11 +1047,65 @@ function rcmail_washtml_callback($tagname, $attrib, $content, $washtml)
+                 $washtml->extlinks = true;
+             }
+             else {
+-                $out = html::tag('style', array('type' => 'text/css'), $decoded);
++                $out = $decoded;
+             }
+-            break;
+         }
+ 
++        if (strlen($out)) {
++            $css_prefix = $washtml->get_config('css_prefix');
++            $is_safe = $washtml->get_config('allow_remote');
++            $body_class = $washtml->get_config('body_class') ?: '';
++            $cont_id = $washtml->get_config('container_id') ?: '';
++            $cont_id = trim($cont_id . ($body_class ? " div.{$body_class}" : ''));
++
++            $out = rcube_utils::mod_css_styles($out, $cont_id, $is_safe, $css_prefix);
++
++            $out = html::tag('style', array('type' => 'text/css'), $out);
++        }
++
++        break;
++
++    case 'body':
++        $style = array();
++        $attrs = $wash_html_body_attrs;
++
++        foreach (html::parse_attrib_string($attrib) as $attr_name => $value) {
++            switch (strtolower($attr_name)) {
++                case 'bgcolor':
++                    // Get bgcolor, we'll set it as background-color of the message container
++                    if (preg_match('/^([a-z0-9#]+)$/i', $value, $m)) {
++                        $style['background-color'] = $value;
++                    }
++                    break;
++                case 'text':
++                    // Get text color, we'll set it as font color of the message container
++                    if (preg_match('/^([a-z0-9#]+)$/i', $value, $m)) {
++                        $style['color'] = $value;
++                    }
++                    break;
++                case 'background':
++                    // Get background, we'll set it as background-image of the message container
++                    if (preg_match('/^([^\s]+)$/', $value, $m)) {
++                        $style['background-image'] = "url({$value})";
++                    }
++                    break;
++                default:
++                    $attrs[$attr_name] = $value;
++            }
++        }
++
++        if (!empty($style)) {
++            foreach ($style as $idx => $val) {
++                $style[$idx] = $idx . ': ' . $val;
++            }
++
++            $attrs['style'] = ($attrs['style'] ? trim($attrs['style'], ';') . '; ' : '') . implode('; ', $style);
++        }
++
++        $out = html::tag('div', $attrs, $content);
++        $wash_html_body_attrs = array();
++        break;
++
+     default:
+         $out = '';
+     }
+@@ -1083,112 +1162,6 @@ function rcmail_part_image_type($part)
+     }
+ }
+ 
+-/**
+- * Modify a HTML message that it can be displayed inside a HTML page
+- */
+-function rcmail_html4inline($body, &$args)
+-{
+-    $last_pos = 0;
+-    $cont_id  = $args['container_id'] . ($args['body_class'] ? ' div.' . $args['body_class'] : '');
+-
+-    // find STYLE tags
+-    while (($pos = stripos($body, '<style', $last_pos)) !== false && ($pos2 = stripos($body, '</style>', $pos+1))) {
+-        $pos = strpos($body, '>', $pos) + 1;
+-        $len = $pos2 - $pos;
+-
+-        // replace all css definitions with #container [def]
+-        $styles = substr($body, $pos, $len);
+-        $styles = rcube_utils::mod_css_styles($styles, $cont_id, $args['safe'], $args['css_prefix']);
+-
+-        $body     = substr_replace($body, $styles, $pos, $len);
+-        $last_pos = $pos2 + strlen($styles) - $len;
+-    }
+-
+-    $replace = array(
+-        // add comments around html and other tags
+-        '/(<!DOCTYPE[^>]*>)/i'          => '<!--\\1-->',
+-        '/(<\?xml[^>]*>)/i'             => '<!--\\1-->',
+-        '/(<\/?html[^>]*>)/i'           => '<!--\\1-->',
+-        '/(<\/?head[^>]*>)/i'           => '<!--\\1-->',
+-        '/(<title[^>]*>.*<\/title>)/Ui' => '<!--\\1-->',
+-        '/(<\/?meta[^>]*>)/i'           => '<!--\\1-->',
+-        // quote <? of php and xml files that are specified as text/html
+-        '/<\?/' => '<?',
+-        '/\?>/' => '?>',
+-    );
+-
+-    $regexp = '/<body([^>]*)/';
+-
+-    // Handle body attributes that doesn't play nicely with div elements
+-    if (preg_match($regexp, $body, $m)) {
+-        $style = array();
+-        $attrs = $m[0];
+-
+-        // Get bgcolor, we'll set it as background-color of the message container
+-        if ($m[1] && preg_match('/bgcolor=["\']*([a-z0-9#]+)["\']*/i', $attrs, $mb)) {
+-            $style['background-color'] = $mb[1];
+-            $attrs = preg_replace('/\s?bgcolor=["\']*[a-z0-9#]+["\']*/i', '', $attrs);
+-        }
+-
+-        // Get text color, we'll set it as font color of the message container
+-        if ($m[1] && preg_match('/text=["\']*([a-z0-9#]+)["\']*/i', $attrs, $mb)) {
+-            $style['color'] = $mb[1];
+-            $attrs = preg_replace('/\s?text=["\']*[a-z0-9#]+["\']*/i', '', $attrs);
+-        }
+-
+-        // Get background, we'll set it as background-image of the message container
+-        if ($m[1] && preg_match('/background=["\']*([^"\'>\s]+)["\']*/', $attrs, $mb)) {
+-            $style['background-image'] = 'url('.$mb[1].')';
+-            $attrs = preg_replace('/\s?background=["\']*([^"\'>\s]+)["\']*/', '', $attrs);
+-        }
+-
+-        if (!empty($style)) {
+-            $body = preg_replace($regexp, rtrim($attrs), $body, 1);
+-        }
+-
+-        // handle body styles related to background image
+-        if ($style['background-image']) {
+-            // get body style
+-            if (preg_match('/#'.preg_quote($cont_id, '/').'\s+\{([^}]+)}/i', $body, $m)) {
+-                // get background related style
+-                $regexp = '/(background-position|background-repeat)\s*:\s*([^;]+);/i';
+-                if (preg_match_all($regexp, $m[1], $matches, PREG_SET_ORDER)) {
+-                    foreach ($matches as $m) {
+-                        $style[$m[1]] = $m[2];
+-                    }
+-                }
+-            }
+-        }
+-
+-        if (!empty($style)) {
+-            foreach ($style as $idx => $val) {
+-                $style[$idx] = $idx . ': ' . $val;
+-            }
+-
+-            $args['container_attrib']['style'] = implode('; ', $style);
+-        }
+-
+-        // replace <body> with <div>
+-        if (!empty($args['body_class'])) {
+-            $replace['/<body([^>]*)>/i'] = '<div class="' . $args['body_class'] . '"\\1>';
+-        }
+-        else {
+-            $replace['/<body/i'] = '<div';
+-        }
+-
+-        $replace['/<\/body>/i'] = '</div>';
+-    }
+-    // make sure there's 'rcmBody' div, we need it for proper css modification
+-    // its name is hardcoded in rcmail_message_body() also
+-    else if (!empty($args['body_class'])) {
+-        $body = '<div class="' . $args['body_class'] . '">' . $body . '</div>';
+-    }
+-
+-    // Clean up, and replace <body> with <div>
+-    $body = preg_replace(array_keys($replace), array_values($replace), $body);
+-
+-    return $body;
+-}
+ 
+ /**
+  * Parse link (a, link, area) attributes and set correct target
+diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc
+index f7c6819..6ddc4bd 100644
+--- a/program/steps/mail/show.inc
++++ b/program/steps/mail/show.inc
+@@ -687,10 +687,6 @@ function rcmail_message_body($attrib)
+                     $OUTPUT->set_env('is_pgp_content', '#' . $container_id);
+                 }
+ 
+-                if ($part->ctype_secondary == 'html') {
+-                    $body = rcmail_html4inline($body, $body_args);
+-                }
+-
+                 $out .= html::div($body_args['container_attrib'], $plugin['prefix'] . $body);
+             }
+         }
+diff --git a/program/steps/settings/save_identity.inc b/program/steps/settings/save_identity.inc
+index 052941e..ab44b77 100644
+--- a/program/steps/settings/save_identity.inc
++++ b/program/steps/settings/save_identity.inc
+@@ -244,6 +244,8 @@ function rcmail_wash_html($html)
+         'charset'       => RCUBE_CHARSET,
+         'html_elements' => array('body', 'link'),
+         'html_attribs'  => array('rel', 'type'),
++        'ignore_elements' => array('body'),
++        'add_comments'  => false,
+     );
+ 
+     // initialize HTML washer
+@@ -255,10 +257,5 @@ function rcmail_wash_html($html)
+     // Remove non-UTF8 characters (#1487813)
+     $html = rcube_charset::clean($html);
+ 
+-    $html = $washer->wash($html);
+-
+-    // remove unwanted comments and tags (produced by washtml)
+-    $html = preg_replace(array('/<!--[^>]+-->/', '/<\/?body>/'), '', $html);
+-
+-    return $html;
++    return $washer->wash($html);
+ }
+diff --git a/tests/MailFunc.php b/tests/MailFunc.php
+index a72b5c9..9c0ae05 100644
+--- a/tests/MailFunc.php
++++ b/tests/MailFunc.php
+@@ -41,11 +41,11 @@ class MailFunc extends \PHPUnit\Framework\TestCase
+         $part = $this->get_html_part('src/htmlbody.txt');
+         $part->replaces = array('ex1.jpg' => 'part_1.2.jpg', 'ex2.jpg' => 'part_1.2.jpg');
+ 
+-        $params = array('container_id' => 'foo');
++        $params = array('container_id' => 'foo', 'safe' => false);
+ 
+         // render HTML in normal mode
+-        $html = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => false)), $params);
+-
++        $html = rcmail_print_body($part->body, $part, $params);
++ 
+         $this->assertMatchesRegularExpression('/src="'.$part->replaces['ex1.jpg'].'"/', $html, "Replace reference to inline image");
+         $this->assertMatchesRegularExpression('#background="program/resources/blocked.gif"#', $html, "Replace external background image");
+         $this->assertDoesNotMatchRegularExpression('/ex3.jpg/', $html, "No references to external images");
+@@ -57,7 +57,8 @@ class MailFunc extends \PHPUnit\Framework\TestCase
+         $this->assertTrue($GLOBALS['REMOTE_OBJECTS'], "Remote object detected");
+ 
+         // render HTML in safe mode
+-        $html2 = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => true)), $params);
++        $params['safe'] = true;
++        $html2 = rcmail_print_body($part->body, $part, $params);
+ 
+         $this->assertMatchesRegularExpression('/<style [^>]+>/', $html2, "Allow styles in safe mode");
+         $this->assertMatchesRegularExpression('#src="http://evilsite.net/mailings/ex3.jpg"#', $html2, "Allow external images in HTML (safe mode)");
+@@ -72,15 +73,12 @@ class MailFunc extends \PHPUnit\Framework\TestCase
+     function test_html_xss()
+     {
+         $part = $this->get_html_part('src/htmlxss.txt');
+-        $washed = rcmail_print_body($part->body, $part, array('safe' => true));
+-
+-        $this->assertDoesNotMatchRegularExpression('/src="skins/', $washed, "Remove local references");
+-        $this->assertDoesNotMatchRegularExpression('/\son[a-z]+/', $washed, "Remove on* attributes");
+-        $this->assertStringNotContainsString('onload', $washed, "Handle invalid style");
+-
+-        $params = array('container_id' => 'foo');
+-        $html   = rcmail_html4inline($washed, $params);
++        $params = array('container_id' => 'foo', 'safe' => true);
++        $html = rcmail_print_body($part->body, $part, $params);
+ 
++        $this->assertDoesNotMatchRegularExpression('/src="skins/', $html, 'Remove local references');
++        $this->assertDoesNotMatchRegularExpression('/\son[a-z]+/', $html, 'Remove on* attributes');
++        $this->assertStringNotContainsString('onload', $html, 'Handle invalid style');
+         $this->assertDoesNotMatchRegularExpression('/onclick="return rcmail.command(\'compose\',\'xss at somehost.net\',this)"/', $html, "Clean mailto links");
+         $this->assertDoesNotMatchRegularExpression('/alert/', $html, "Remove alerts");
+     }
+@@ -93,7 +91,7 @@ class MailFunc extends \PHPUnit\Framework\TestCase
+     {
+         $part   = $this->get_html_part('src/BID-26800.txt');
+         $params = array('container_id' => 'dabody', 'safe' => true);
+-        $washed = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => true)), $params);
++        $washed = rcmail_print_body($part->body, $part, $params);
+ 
+         $this->assertDoesNotMatchRegularExpression('/alert|expression|javascript|xss/', $washed, "Remove evil style blocks");
+         $this->assertDoesNotMatchRegularExpression('/font-style:italic/', $washed, "Allow valid styles");
+@@ -113,20 +111,40 @@ class MailFunc extends \PHPUnit\Framework\TestCase
+         $this->assertDoesNotMatchRegularExpression('/vbscript:/', $washed, "Remove vbscript: links");
+     }
+ 
++    /**
++     * Test that HTML sanitization does not change attribute (evil) values
++     */
++    public function test_html_body_attributes()
++    {
++        $part = $this->get_html_part();
++        $part->body = '<body title="bgcolor=foo" name="bar style=animation-name:progress-bar-stripes onanimationstart=alert(origin) foo=bar">Foo</body>';
++
++        $params = array('safe' => true, 'add_comments' => false);
++        $washed = rcmail_print_body($part->body, $part, $params);
++
++        $this->assertSame(str_replace('body', 'div', $part->body) . "\n", $washed);
++
++        $params['inline_html'] = false;
++        $washed = rcmail_print_body($part->body, $part, $params);
++
++        $this->assertSame('<html><head></head>' . $part->body . '</html>', $washed);
++    }
++
+     /**
+      * Test handling of body style attributes
+      */
+-    function test_html4inline_body_style()
++    public function test_wash_html_body_style()
+     {
+-        $html   = '<body background="test" bgcolor="#fff" style="font-size:11px" text="#000"><p>test</p></body>';
+-        $params = array('container_id' => 'foo');
+-        $html   = rcmail_html4inline($html, $params);
+-
+-        $this->assertMatchesRegularExpression('/<div style="font-size:11px">/', $html, "Body attributes");
+-        $this->assertArrayHasKey('container_attrib', $params, "'container_attrib' param set");
+-        $this->assertMatchesRegularExpression('/background-color: #fff;/', $params['container_attrib']['style'], "Body style (bgcolor)");
+-        $this->assertMatchesRegularExpression('/background-image: url\(test\)/', $params['container_attrib']['style'], "Body style (background)");
+-        $this->assertMatchesRegularExpression('/color: #000/', $params['container_attrib']['style'], "Body style (text)");
++        $html = '<body background="http://test.com/image" bgcolor="#fff" style="font-size: 11px" text="#000"><p>test</p></body>';
++        $params = array('container_id' => 'foo', 'add_comments' => false, 'safe' => false);
++        $washed = rcmail_wash_html($html, $params, array());
++
++        $this->assertSame('<div id="foo" style="font-size: 11px; background-image: url(program/resources/blocked.gif); background-color: #fff; color: #000"><p>test</p></div>' . "\n", $washed);
++
++        $params['safe'] = true;
++        $washed = rcmail_wash_html($html, $params, array());
++ 
++        $this->assertSame('<div id="foo" style="font-size: 11px; background-image: url(http://test.com/image); background-color: #fff; color: #000"><p>test</p></div>' . "\n", $washed);
+     }
+ 
+     /**
+@@ -149,6 +167,7 @@ class MailFunc extends \PHPUnit\Framework\TestCase
+     {
+         $meta = '<meta charset="'.RCUBE_CHARSET.'" />';
+         $args = array(
++            'inline_html' => false,
+             'html_elements' => array('html', 'body', 'meta', 'head'),
+             'html_attribs'  => array('charset'),
+         );
+@@ -201,10 +220,10 @@ class MailFunc extends \PHPUnit\Framework\TestCase
+     function test_mailto()
+     {
+         $part = $this->get_html_part('src/mailto.txt');
+-        $params = array('container_id' => 'foo');
++        $params = array('container_id' => 'foo', 'safe' => false);
+ 
+         // render HTML in normal mode
+-        $html = rcmail_html4inline(rcmail_print_body($part->body, $part, array('safe' => false)), $params);
++        $html = rcmail_print_body($part->body, $part, $params);
+ 
+         $mailto = '<a href="mailto:me at me.com"'
+             .' onclick="return rcmail.command(\'compose\',\'me at me.com?subject=this is the subject&body=this is the body\',this)" rel="noreferrer">e-mail</a>';
diff -Nru roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42010.patch roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42010.patch
--- roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42010.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.4.15+dfsg.1/debian/patches/CVE-2024-42010.patch	2024-08-08 23:48:56.000000000 +0200
@@ -0,0 +1,611 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Sat, 3 Aug 2024 09:24:19 +0200
+Subject: Fix information leak (access to remote content) via insufficient CSS
+ filtering
+
+Credits to Oskar Zeino-Mahmalat (Sonar) https://www.sonarsource.com
+
+Origin: https://github.com/roundcube/roundcubemail/commit/53da61f7fc8a5d8414c36c45e44901d2a568319a
+Origin: https://github.com/roundcube/roundcubemail/commit/9f19b931e3b89c2fa577e2bf719f7db84492eb66
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-42010
+Bug-Debian: https://bugs.debian.org/1077969
+---
+ program/lib/Roundcube/rcube_utils.php   | 236 ++++++++++++++++++++++++++------
+ program/lib/Roundcube/rcube_washtml.php |  95 +------------
+ tests/Framework/Utils.php               | 146 +++++++++++++++++---
+ tests/Framework/Washtml.php             |   2 +-
+ 4 files changed, 327 insertions(+), 152 deletions(-)
+
+diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php
+index d427352..68f8684 100644
+--- a/program/lib/Roundcube/rcube_utils.php
++++ b/program/lib/Roundcube/rcube_utils.php
+@@ -373,19 +373,40 @@ class rcube_utils
+      */
+     public static function mod_css_styles($source, $container_id, $allow_remote = false, $prefix = '')
+     {
+-        $last_pos     = 0;
+-        $replacements = new rcube_string_replacer;
+-
+-        // ignore the whole block if evil styles are detected
+         $source   = self::xss_entity_decode($source);
+-        $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
+-        $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\((?!data:image)' : '');
+ 
+-        if (preg_match("/$evilexpr/i", $stripped)) {
++        // No @import allowed
++        // TODO: We should just remove it, not invalidate the whole content
++        if (stripos($source, '@import') !== false) {
++            return '/* evil! */';
++        }
++
++        // Incomplete style expression
++        if (strpos($source, '{') === false) {
++            return '/* invalid! */';
++        }
++
++        // To prevent from a double-escaping tricks we consider a script with
++        // any escape sequences (after de-escaping them above) an evil script.
++        // This probably catches many valid scripts, but we\'re on the safe side.
++        if (preg_match('/\\\[0-9a-fA-F]{2}/', $source)) {
+             return '/* evil! */';
+         }
+ 
+-        $strict_url_regexp = '!url\s*\(\s*["\']?(https?:)//[a-z0-9/._+-]+["\']?\s*\)!Uims';
++        // remove html comments
++        $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source);
++
++        $url_callback = static function ($url) use ($allow_remote) {
++            if (strpos($url, 'data:image') === 0) {
++                return $url;
++            }
++            if ($allow_remote && preg_match('|^https?://[a-z0-9/._+-]+$|i', $url)) {
++                return $url;
++            }
++        };
++
++        $last_pos = 0;
++        $replacements = new rcube_string_replacer();
+ 
+         // cut out all contents between { and }
+         while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
+@@ -394,45 +415,14 @@ class rcube_utils
+                 $pos = $nested;
+             $length = $pos2 - $pos - 1;
+             $styles = substr($source, $pos+1, $length);
++            $styles = self::sanitize_css_block($styles, $url_callback);
+ 
+-            // Convert position:fixed to position:absolute (#5264)
+-            $styles = preg_replace('/position[^a-z]*:[\s\r\n]*fixed/i', 'position: absolute', $styles);
+-
+-            // Remove 'page' attributes (#7604)
+-            $styles = preg_replace('/((^|[\n\s;])page:)[^;]+;*/im', '\\1 unset;', $styles);
+-
+-            // check every line of a style block...
+-            if ($allow_remote) {
+-                $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
+-
+-                for ($i=0, $len=count($a_styles); $i < $len; $i++) {
+-                    $line     = $a_styles[$i];
+-                    $stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
+-
+-                    // allow data:image uri, join with continuation
+-                    if (stripos($stripped, 'url(data:image')) {
+-                        $a_styles[$i] .= ';' . $a_styles[$i+1];
+-                        unset($a_styles[$i+1]);
+-                    }
+-                    // allow strict url() values only
+-                    else if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) {
+-                        $a_styles = array('/* evil! */');
+-                        break;
+-                    }
+-                }
+-
+-                $styles = implode(";\n", $a_styles);
+-            }
+-
+-            $key      = $replacements->add($styles);
++            $key      = $replacements->add(strlen($styles) ? " {$styles} " : '');
+             $repl     = $replacements->get_replacement($key);
+             $source   = substr_replace($source, $repl, $pos+1, $length);
+             $last_pos = $pos2 - ($length - strlen($repl));
+         }
+ 
+-        // remove html comments
+-        $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source);
+-
+         // add #container to each tag selector and prefix to id/class identifiers
+         if ($container_id || $prefix) {
+             // Exclude rcube_string_replacer pattern matches, this is needed
+@@ -475,6 +465,170 @@ class rcube_utils
+         return $source;
+     }
+ 
++    /**
++     * Parse and sanitize single CSS block
++     *
++     * @param string    $styles       CSS styles block
++     * @param ?callable $url_callback URL validator callback
++     *
++     * @return string
++     */
++    public static function sanitize_css_block($styles, $url_callback = null)
++    {
++        $output = array();
++
++        // check every css rule in the style block...
++        foreach (self::parse_css_block($styles) as $rule) {
++            $property = $rule[0];
++            $value = $rule[1];
++
++            if ($property == 'page') {
++                // Remove 'page' attributes (#7604)
++                continue;
++            } elseif ($property == 'position' && strcasecmp($value, 'fixed') === 0) {
++                // Convert position:fixed to position:absolute (#5264)
++                $value = 'absolute';
++            } elseif (preg_match('/expression|image-set/i', $value)) {
++                continue;
++            } else {
++                $value = '';
++                foreach (self::explode_css_property_block($rule[1]) as $val) {
++                    if ($url_callback && preg_match('/^url\s*\(/i', $val)) {
++                        if (preg_match('/^url\s*\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
++                            if ($url = $url_callback($match[1])) {
++                                $value .= ' url(' . $url . ')';
++                            }
++                        }
++                    } else {
++                        // whitelist ?
++                        $value .= ' ' . $val;
++
++                        // #1488535: Fix size units, so width:800 would be changed to width:800px
++                        if ($val
++                            && preg_match('/^(left|right|top|bottom|width|height)/i', $property)
++                            && preg_match('/^[0-9]+$/', $val)
++                        ) {
++                            $value .= 'px';
++                        }
++                    }
++                }
++            }
++
++            if (strlen($value)) {
++                $output[] = $property . ': ' . trim($value);
++            }
++        }
++
++        return count($output) > 0 ? implode('; ', $output) . ';' : '';
++    }
++
++    /**
++     * Explode css style. Property names will be lower-cased and trimmed.
++     * Values will be trimmed. Invalid entries will be skipped.
++     *
++     * @param string $style CSS style
++     *
++     * @return array List of CSS rule pairs, e.g. [['color', 'red'], ['top', '0']]
++     */
++    public static function parse_css_block($style)
++    {
++        $pos = 0;
++
++        // first remove comments
++        while (($pos = strpos($style, '/*', $pos)) !== false) {
++            $end = strpos($style, '*/', $pos+2);
++
++            if ($end === false) {
++                $style = substr($style, 0, $pos);
++            }
++            else {
++                $style = substr_replace($style, '', $pos, $end - $pos + 2);
++            }
++        }
++
++        // Replace new lines with spaces
++        $style = preg_replace('/[\r\n]+/', ' ', $style);
++
++        $style  = trim($style);
++        $length = strlen($style);
++        $result = array();
++        $pos    = 0;
++
++        while ($pos < $length && ($colon_pos = strpos($style, ':', $pos))) {
++            // Property name
++            $name = strtolower(trim(substr($style, $pos, $colon_pos - $pos)));
++
++            // get the property value
++            $q = $s = false;
++            for ($i = $colon_pos + 1; $i < $length; $i++) {
++                if (($style[$i] == "\"" || $style[$i] == "'") && ($i == 0 || $style[$i-1] != "\\")) {
++                    if ($q == $style[$i]) {
++                        $q = false;
++                    }
++                    else if ($q === false) {
++                        $q = $style[$i];
++                    }
++                }
++                else if ($style[$i] == "(" && !$q && ($i == 0 || $style[$i-1] != "\\")) {
++                    $q = "(";
++                }
++                else if ($style[$i] == ")" && $q == "(" && $style[$i-1] != "\\") {
++                    $q = false;
++                }
++
++                if ($q === false && (($s = $style[$i] == ';') || $i == $length - 1)) {
++                    break;
++                }
++            }
++
++            $value_length = $i - $colon_pos - ($s ? 1 : 0);
++            $value        = trim(substr($style, $colon_pos + 1, $value_length));
++
++            if (strlen($name) && !preg_match('/[^a-z-]/', $name) && strlen($value) && $value !== ';') {
++                $result[] = array($name, $value);
++            }
++
++            $pos = $i + 1;
++        }
++
++        return $result;
++    }
++
++    /**
++     * Explode css style value
++     *
++     * @param string $style CSS style
++     *
++     * @return array List of CSS values
++     */
++    public static function explode_css_property_block($style)
++    {
++        $style = preg_replace('/\s+/', ' ', $style);
++        $result = array();
++        $strlen = strlen($style);
++        $q = false;
++
++        // explode value
++        for ($p = $i = 0; $i < $strlen; $i++) {
++            if (($style[$i] == '"' || $style[$i] == "'") && ($i == 0 || $style[$i - 1] != '\\')) {
++                if ($q == $style[$i]) {
++                    $q = false;
++                } elseif (!$q) {
++                    $q = $style[$i];
++                }
++            }
++
++            if (!$q && $style[$i] == ' ' && ($i == 0 || !preg_match('/[,\(]/', $style[$i - 1]))) {
++                $result[] = substr($style, $p, $i - $p);
++                $p = $i + 1;
++            }
++        }
++
++        $result[] = (string) substr($style, $p);
++
++        return $result;
++    }
++
+     /**
+      * Generate CSS classes from mimetype and filename extension
+      *
+diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
+index 8a3a5ab..43c9274 100644
+--- a/program/lib/Roundcube/rcube_washtml.php
++++ b/program/lib/Roundcube/rcube_washtml.php
+@@ -249,51 +249,19 @@ class rcube_washtml
+     {
+         $result = array();
+ 
+-        // Remove unwanted white-space characters so regular expressions below work better
+-        $style = preg_replace('/[\n\r\s\t]+/', ' ', $style);
+-
+         // Decode insecure character sequences
+         $style = rcube_utils::xss_entity_decode($style);
+ 
+-        foreach (explode(';', $style) as $declaration) {
+-            if (preg_match('/^\s*([a-z\\\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
+-                $cssid = $match[1];
+-                $str   = $match[2];
+-                $value = '';
+-
+-                foreach ($this->explode_style($str) as $val) {
+-                    if (preg_match('/^url\(/i', $val)) {
+-                        if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
+-                            if ($url = $this->wash_uri($match[1])) {
+-                                $value .= ' url(' . htmlspecialchars($url, ENT_QUOTES, $this->config['charset']) . ')';
+-                            }
+-                        }
+-                    }
+-                    else if (!preg_match('/^(behavior|expression)/i', $val)) {
+-                        // Set position:fixed to position:absolute for security (#5264)
+-                        if (!strcasecmp($cssid, 'position') && !strcasecmp($val, 'fixed')) {
+-                            $val = 'absolute';
+-                        }
+-
+-                        // whitelist ?
+-                        $value .= ' ' . $val;
+-
+-                        // #1488535: Fix size units, so width:800 would be changed to width:800px
+-                        if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid)
+-                            && preg_match('/^[0-9]+$/', $val)
+-                        ) {
+-                            $value .= 'px';
+-                        }
+-                    }
+-                }
++        // Remove unwanted white-space characters
++        $style = preg_replace('/[\n\r\t]+/', ' ', $style);
+ 
+-                if (isset($value[0])) {
+-                    $result[] = $cssid . ':' . $value;
+-                }
++        $uri_callback = function ($uri) {
++            if ($uri = $this->wash_uri($uri)) {
++                return htmlspecialchars($uri, \ENT_QUOTES, $this->config['charset']);
+             }
+-        }
++        };
+ 
+-        return implode('; ', $result);
++        return rtrim(rcube_utils::sanitize_css_block($style, $uri_callback), ';');
+     }
+ 
+     /**
+@@ -970,53 +938,4 @@ class rcube_washtml
+ 
+         return $html;
+     }
+-
+-    /**
+-     * Explode css style value
+-     *
+-     * @param string $style CSS style
+-     *
+-     * @return array List of CSS rules
+-     */
+-    protected function explode_style($style)
+-    {
+-        $pos = 0;
+-
+-        // first remove comments
+-        while (($pos = strpos($style, '/*', $pos)) !== false) {
+-            $end = strpos($style, '*/', $pos+2);
+-
+-            if ($end === false) {
+-                $style = substr($style, 0, $pos);
+-            }
+-            else {
+-                $style = substr_replace($style, '', $pos, $end - $pos + 2);
+-            }
+-        }
+-
+-        $style  = trim($style);
+-        $strlen = strlen($style);
+-        $result = array();
+-
+-        // explode value
+-        for ($p=$i=0; $i < $strlen; $i++) {
+-            if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
+-                if ($q == $style[$i]) {
+-                    $q = false;
+-                }
+-                else if (!$q) {
+-                    $q = $style[$i];
+-                }
+-            }
+-
+-            if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
+-                $result[] = substr($style, $p, $i - $p);
+-                $p = $i + 1;
+-            }
+-        }
+-
+-        $result[] = (string) substr($style, $p);
+-
+-        return $result;
+-    }
+ }
+diff --git a/tests/Framework/Utils.php b/tests/Framework/Utils.php
+index b7922e3..4bd9e52 100644
+--- a/tests/Framework/Utils.php
++++ b/tests/Framework/Utils.php
+@@ -182,8 +182,8 @@ class Framework_Utils extends \PHPUnit\Framework\TestCase
+         $mod = rcube_utils::mod_css_styles($css, 'rcmbody');
+ 
+         $this->assertStringContainsString('#rcmbody table[class=w600]', $mod, 'Replace styles nested in @media block');
+-        $this->assertStringContainsString('#rcmbody {width:600px', $mod, 'Replace body selector nested in @media block');
+-        $this->assertStringContainsString('#rcmbody {min-width:474px', $mod, 'Replace body selector nested in @media block (#5811)');
++        $this->assertStringContainsString('#rcmbody { width: 600px', $mod, 'Replace body selector nested in @media block');
++        $this->assertStringContainsString('#rcmbody { min-width: 474px', $mod, 'Replace body selector nested in @media block (#5811)');
+     }
+ 
+     /**
+@@ -191,26 +191,48 @@ class Framework_Utils extends \PHPUnit\Framework\TestCase
+      */
+     function test_mod_css_styles_xss()
+     {
+-        $mod = rcube_utils::mod_css_styles("body.main2cols { background-image: url('../images/leftcol.png'); }", 'rcmbody');
+-        $this->assertEquals("/* evil! */", $mod, "No url() values allowed");
++        $mod = \rcube_utils::mod_css_styles('font-size: 1em;', 'rcmbody');
++        $this->assertSame('/* invalid! */', $mod);
+ 
+         $mod = rcube_utils::mod_css_styles("@import url('http://localhost/somestuff/css/master.css');", 'rcmbody');
+-        $this->assertEquals("/* evil! */", $mod, "No import statements");
++        $this->assertSame('/* evil! */', $mod);
+ 
+-        $mod = rcube_utils::mod_css_styles("left:expression(document.body.offsetWidth-20)", 'rcmbody');
+-        $this->assertEquals("/* evil! */", $mod, "No expression properties");
++        $mod = \rcube_utils::mod_css_styles("@\\69mport url('http://localhost/somestuff/css/master.css');", 'rcmbody');
++        $this->assertSame('/* evil! */', $mod);
+ 
+-        $mod = rcube_utils::mod_css_styles("left:exp/*  */ression( alert('xss3') )", 'rcmbody');
+-        $this->assertEquals("/* evil! */", $mod, "Don't allow encoding quirks");
++        $mod = \rcube_utils::mod_css_styles("@\\5C 69mport url('http://localhost/somestuff/css/master.css'); a { color: red; }", '');
++        $this->assertSame('/* evil! */', $mod);
+ 
+-        $mod = rcube_utils::mod_css_styles("background:\\0075\\0072\\00006c( javascript:alert('xss') )", 'rcmbody');
+-        $this->assertEquals("/* evil! */", $mod, "Don't allow encoding quirks (2)");
++        $mod = \rcube_utils::mod_css_styles("body.main2cols { background-image: url('../images/leftcol.png'); }", 'rcmbody');
++        $this->assertSame('#rcmbody.main2cols {}', $mod);
+ 
+-        $mod = rcube_utils::mod_css_styles("background: \\75 \\72 \\6C ('/images/img.png')", 'rcmbody');
+-        $this->assertEquals("/* evil! */", $mod, "Don't allow encoding quirks (3)");
++        $mod = \rcube_utils::mod_css_styles('p { left:expression(document.body.offsetWidth-20); }', 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
+ 
+-        $mod = rcube_utils::mod_css_styles("background: u\\r\\l('/images/img.png')", 'rcmbody');
+-        $this->assertEquals("/* evil! */", $mod, "Don't allow encoding quirks (4)");
++        $mod = \rcube_utils::mod_css_styles('p { left:exp/*  */ression( alert('xss3') ); }', 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
++
++        $mod = \rcube_utils::mod_css_styles("p { background:\\0075\\0072\\00006c('//evil.com/test'); }", 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
++
++        $mod = \rcube_utils::mod_css_styles("p { background: \\75 \\72 \\6C ('/images/img.png'); }", 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
++
++        $mod = \rcube_utils::mod_css_styles("p { background: u\\r\\l('/images/img2.png'); }", 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
++
++        $mod = \rcube_utils::mod_css_styles("p { background: url('//żą.ść?data:image&leak') }", 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
++
++        $mod = \rcube_utils::mod_css_styles("p { background: url('//data:image&leak'); }", 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
++
++        // Note: This looks to me like a bug in browsers, for now we don't allow image-set at all
++        $mod = \rcube_utils::mod_css_styles("p { background: image-set('//evil.com/img.png' 1x); }", 'rcmbody');
++        $this->assertSame('#rcmbody p {}', $mod);
++
++        $mod = \rcube_utils::mod_css_styles('p { background: none !important; }', 'rcmbody');
++        $this->assertSame('#rcmbody p { background: none !important; }', $mod);
+ 
+         // position: fixed (#5264)
+         $mod = rcube_utils::mod_css_styles(".test { position: fixed; }", 'rcmbody');
+@@ -241,19 +263,20 @@ class Framework_Utils extends \PHPUnit\Framework\TestCase
+         // XSS issue, HTML in 'content' property
+         $style = "body { content: '</style><img src onerror=\"alert(\'hello\');\">'; color: red; }";
+         $mod = rcube_utils::mod_css_styles($style, 'rcmbody', true);
+-        $this->assertSame("#rcmbody { content: '';\n color: red;\n }", $mod);
++        $this->assertSame("#rcmbody { content: ''; color: red; }", $mod);
+ 
+         $style = "body { content: '< page: ;/style>< page: ;img src onerror=\"alert(\'hello\');\">'; color: red; }";
+         $mod = rcube_utils::mod_css_styles($style, 'rcmbody', true);
+-        $this->assertSame(
+-            "#rcmbody { content: '< page: unset;/style>< page: unset;img src onerror=\"alert('hello');\">'; color: red; }",
+-            str_replace("\n", '', $mod)
+-        );
++        $this->assertSame("#rcmbody { content: '< page: ;/style>< page: ;img src onerror=\"alert('hello');\">'; color: red; }", $mod);
+ 
+         // Removing page: property
+-        $style = "body { page: test; color: red; }";
++        $style = "body { page: test; color: red }";
+         $mod = rcube_utils::mod_css_styles($style, 'rcmbody', true);
+-        $this->assertSame("#rcmbody { page: unset;\n color: red;\n }", $mod);
++        $this->assertSame("#rcmbody { color: red; }", $mod);
++
++        $style = "body { background:url(alert('URL!')); }";
++        $mod = rcube_utils::mod_css_styles($style, 'rcmbody', true);
++        $this->assertSame("#rcmbody {}", $mod);
+     }
+ 
+     /**
+@@ -308,6 +331,85 @@ class Framework_Utils extends \PHPUnit\Framework\TestCase
+         $this->assertStringContainsString('#foo', $mod, "Strip HTML comments from content, but not the content");
+     }
+ 
++    /**
++     * Test-Cases for parse_css_block() test
++     */
++    function data_parse_css_block()
++    {
++        return [
++            [
++                'test:test2',
++                [['test', 'test2']],
++            ],
++            [
++                'Test :teSt2 ;',
++                [['test', 'teSt2']],
++            ],
++            [
++                'test : test2 test3;',
++                [['test', 'test2 test3']],
++            ],
++            [
++                '/* : */ test : val /* ; */ ;',
++                [['test', 'val']],
++            ],
++            [
++                '/* test : val */ ;',
++                [],
++            ],
++            [
++                'test :"test1\\"test2" ;',
++                [['test', '"test1\\"test2"']],
++            ],
++            [
++                "test : 'test5 \\'test6';",
++                [['test', "'test5 \\'test6'"]],
++            ],
++            [
++                "test: test8\ntest6;",
++                [['test', 'test8 test6']],
++            ],
++            [
++                "PRop: val1; prop-two: 'val2: '",
++                [['prop', 'val1'], ['prop-two', "'val2: '"]],
++            ],
++            [
++                "prop: val1; prop-two :",
++                [['prop', 'val1']],
++            ],
++            [
++                "prop: val1; prop-two :;",
++                [['prop', 'val1']],
++            ],
++            [
++                "background: url(data:image/png;base64,test)",
++                [['background', 'url(data:image/png;base64,test)']],
++            ],
++            [
++                "background:url('data:image/png;base64,test')",
++                [['background', "url('data:image/png;base64,test')"]],
++            ],
++            [
++                "background: url(\"data:image/png;base64,test\")",
++                [['background', 'url("data:image/png;base64,test")']],
++            ],
++            [
++                'font-family:"新細明體","serif";color:red',
++                [['font-family', '"新細明體","serif"'], ['color', 'red']]
++            ],
++        ];
++    }
++
++    /**
++     * Test parse_css_block()
++     *
++     * @dataProvider data_parse_css_block
++     */
++    function test_explode_style($input, $output)
++    {
++        $this->assertSame($output, rcube_utils::parse_css_block($input));
++    }
++
+     /**
+      * Check rcube_utils::explode_quoted_string()
+      */
+diff --git a/tests/Framework/Washtml.php b/tests/Framework/Washtml.php
+index 6848eb2..afddedf 100644
+--- a/tests/Framework/Washtml.php
++++ b/tests/Framework/Washtml.php
+@@ -252,7 +252,7 @@ class Framework_Washtml extends \PHPUnit\Framework\TestCase
+         $washer = new rcube_washtml;
+         $washed = $washer->wash($html);
+ 
+-        $this->assertTrue(strpos($washed, $expected) !== false, "White-space and new-line characters handling");
++        $this->assertSame($this->cleanupResult($washed), $expected, 'White-space and new-line characters handling');
+     }
+ 
+     /**
diff -Nru roundcube-1.4.15+dfsg.1/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch roundcube-1.4.15+dfsg.1/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch
--- roundcube-1.4.15+dfsg.1/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.4.15+dfsg.1/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch	2024-08-08 23:48:56.000000000 +0200
@@ -0,0 +1,52 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Wed, 31 Jul 2024 18:11:31 +0200
+Subject: Fix infinite loop when parsing malformed Sieve script
+
+Origin: https://github.com/roundcube/roundcubemail/commit/3567090a997e95aac6bb052bfb48bb301d0c03c3
+Bug: https://github.com/roundcube/roundcubemail/issues/9562
+---
+ plugins/managesieve/lib/Roundcube/rcube_sieve_script.php |  7 ++++---
+ plugins/managesieve/tests/Parser.php                     | 14 ++++++++++++++
+ 2 files changed, 18 insertions(+), 3 deletions(-)
+
+diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
+index 4b70b5e..46923d3 100644
+--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
++++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
+@@ -1305,9 +1305,10 @@ class rcube_sieve_script
+ 
+             // bracket-comment
+             case '/':
+-                if ($str[$position + 1] == '*') {
+-                    if ($end_pos = strpos($str, '*/', $position + 2)) {
+-                        $position = $end_pos + 2;
++                $position++;
++                if ($str[$position] == '*') {
++                    if ($end_pos = strpos($str, '*/', $position + 1)) {
++                        $position = $end_pos + 1;
+                     }
+                     else {
+                         // error
+diff --git a/plugins/managesieve/tests/Parser.php b/plugins/managesieve/tests/Parser.php
+index e7d511d..537bed0 100644
+--- a/plugins/managesieve/tests/Parser.php
++++ b/plugins/managesieve/tests/Parser.php
+@@ -59,4 +59,18 @@ class Parser extends \PHPUnit\Framework\TestCase
+ 
+         return $result;
+     }
++
++    /**
++     * Sieve script parsing
++     */
++    public function test_parser_bug9562()
++    {
++        // This is an obviously invalid script
++        $input = "vacation :subject \"a\" :from \"b\"\n<a href=\"https://test.org/\">test</a>";
++        $script = new rcube_sieve_script($input);
++        $result = $script->as_text();
++
++        // TODO: The output still is BS, but it at least does not cause an infinite loop
++        $this->assertSame("require [\"vacation\"];\r\nvacation :subject \"a\" :from \"b\" \"a\";\r\n", $result);
++    }
+ }
diff -Nru roundcube-1.4.15+dfsg.1/debian/patches/Fix-regression-where-printing-scaling-rotating-image-atta.patch roundcube-1.4.15+dfsg.1/debian/patches/Fix-regression-where-printing-scaling-rotating-image-atta.patch
--- roundcube-1.4.15+dfsg.1/debian/patches/Fix-regression-where-printing-scaling-rotating-image-atta.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.4.15+dfsg.1/debian/patches/Fix-regression-where-printing-scaling-rotating-image-atta.patch	2024-08-08 23:48:56.000000000 +0200
@@ -0,0 +1,71 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Thu, 8 Aug 2024 13:54:32 +0200
+Subject: Fix regression where printing/scaling/rotating image attachments was
+ broken
+
+Origin: https://github.com/roundcube/roundcubemail/commit/44cec17e8f1b9a03af75f97a9cb6a77724586c47
+Bug: https://github.com/roundcube/roundcubemail/issues/9571
+Bug-Debian: https://bugs.debian.org/1078456
+---
+ program/js/app.js                      | 19 +++++++------------
+ program/lib/Roundcube/rcube_output.php |  3 ++-
+ 2 files changed, 9 insertions(+), 13 deletions(-)
+
+diff --git a/program/js/app.js b/program/js/app.js
+index d95bf3c..4aedf1d 100644
+--- a/program/js/app.js
++++ b/program/js/app.js
+@@ -422,13 +422,11 @@ function rcube_webmail()
+               var contents = $(this).contents();
+ 
+               // do not apply styles to an error page (with no image)
+-              if (contents.find('img').length)
+-                contents.find('head').append(
+-                  '<style type="text/css">'
+-                  + 'img { max-width:100%; max-height:100%; } ' // scale
+-                  + 'body { display:flex; align-items:center; justify-content:center; height:100%; margin:0; }' // align
+-                  + '</style>'
+-                );
++              if (contents.find('img').length) {
++                contents.find('img').css({ maxWidth: '100%', maxHeight: '100%' });
++                contents.find('body').css({ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', margin: 0 });
++                contents.find('html').css({ height: '100%' });
++              }
+             });
+         }
+         // show printing dialog
+@@ -5725,9 +5723,7 @@ function rcube_webmail()
+   this.apply_image_style = function()
+   {
+     var style = [],
+-      head = $(this.gui_objects.messagepartframe).contents().find('head');
+-
+-    $('#image-style', head).remove();
++      img = $(this.gui_objects.messagepartframe).contents().find('img');
+ 
+     $.each({scale: '', rotate: 'deg'}, function(i, v) {
+       var val = ref.image_style[i];
+@@ -5735,8 +5731,7 @@ function rcube_webmail()
+         style.push(i + '(' + val + v + ')');
+     });
+ 
+-    if (style)
+-      head.append($('<style id="image-style">').text('img { transform: ' + style.join(' ') + '}'));
++    img.css('transform', style.join(' '));
+   };
+ 
+   // Update import dialog state
+diff --git a/program/lib/Roundcube/rcube_output.php b/program/lib/Roundcube/rcube_output.php
+index a51b75b..e070c5c 100644
+--- a/program/lib/Roundcube/rcube_output.php
++++ b/program/lib/Roundcube/rcube_output.php
+@@ -286,7 +286,8 @@ abstract class rcube_output
+         }
+ 
+         // Use strict security policy to make sure no javascript content is executed
+-        header("Content-Security-Policy: default-src 'none'");
++        // img-src is needed to be able to print attachment preview page
++        header("Content-Security-Policy: default-src 'none'; img-src 'self'");
+ 
+         // don't kill the connection if download takes more than 30 sec.
+         if (!array_key_exists('time_limit', $params)) {
diff -Nru roundcube-1.4.15+dfsg.1/debian/patches/series roundcube-1.4.15+dfsg.1/debian/patches/series
--- roundcube-1.4.15+dfsg.1/debian/patches/series	2024-06-17 04:10:38.000000000 +0200
+++ roundcube-1.4.15+dfsg.1/debian/patches/series	2024-08-08 23:48:56.000000000 +0200
@@ -22,3 +22,8 @@
 CVE-2023-47272.patch
 CVE-2024-37384.patch
 CVE-2024-37383.patch
+CVE-2024-42009.patch
+CVE-2024-42008.patch
+Fix-regression-where-printing-scaling-rotating-image-atta.patch
+CVE-2024-42010.patch
+Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 833 bytes
Desc: not available
URL: <http://alioth-lists.debian.net/pipermail/pkg-roundcube-maintainers/attachments/20240812/a14c8ab6/attachment-0001.sig>


More information about the Pkg-roundcube-maintainers mailing list