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

Guilhem Moulin guilhem at debian.org
Tue Aug 6 17:31:37 BST 2024


Dear security team,

I misjudged the severity of CVE-2024-42009 when filing #1077969.  The
XSS appears to be critical, see the reporter's report at
https://www.sonarsource.com/blog/government-emails-at-risk-critical-cross-site-scripting-vulnerability-in-roundcube-webmail/

I'd like to propose the attached tested debdiffs to fix that
vulnerability as well as CVE-2024-42008, CVE-2024-42010, and a couple of
of other issues.  All patches were cherry-picked from upstream's 1.6.8
release.

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.

I suggest not to wait for the bookworm-security upload.  Unless you
don't want to issue a bookworm-only DSA while bullseye is still under
your watch, that is. :-)

Cheers
-- 
Guilhem.
-------------- next part --------------
diffstat for roundcube-1.6.5+dfsg roundcube-1.6.5+dfsg

 changelog                                                               |   18 
 patches/CVE-2024-42008.patch                                            |   71 +
 patches/CVE-2024-42009.patch                                            |  624 ++++++++++
 patches/CVE-2024-42010.patch                                            |  408 ++++++
 patches/Fix-bug-where-an-unhandled-exception-was-caused-by-an-inv.patch |  223 +++
 patches/Fix-bug-where-imap_conn_option-s-socket-was-ignored.patch       |   25 
 patches/Fix-fatal-error-when-parsing-some-TNEF-attachments.patch        |  128 ++
 patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch     |   54 
 patches/series                                                          |    7 
 9 files changed, 1558 insertions(+)

diff -Nru roundcube-1.6.5+dfsg/debian/changelog roundcube-1.6.5+dfsg/debian/changelog
--- roundcube-1.6.5+dfsg/debian/changelog	2024-06-17 03:15:26.000000000 +0200
+++ roundcube-1.6.5+dfsg/debian/changelog	2024-08-06 16:02:54.000000000 +0200
@@ -1,3 +1,21 @@
+roundcube (1.6.5+dfsg-1+deb12u3) bookworm-security; urgency=high
+
+  * Cherry pick upstream security fixes from v1.6.8 (closes: #1077969):
+    + CVE-2024-42008: Cross-site scripting (XSS) vulnerability in serving of
+      attachments other than HTML or SVG.
+    + CVE-2024-42009: Cross-site scripting (XSS) vulnerability in
+      post-processing of sanitized HTML content.
+    + CVE-2024-42010: Fix information leak (access to remote content) via
+      insufficient CSS filtering.
+  * Cherry pick further upstream changes from v1.6.8:
+    + Fix fatal error when parsing some TNEF attachments.
+    + Fix bug where an unhandled exception was caused by an invalid image
+      attachment.
+    + Fix infinite loop when parsing malformed Sieve script.
+    + Fix bug where imap_conn_option's 'socket' was ignored.
+
+ -- Guilhem Moulin <guilhem at debian.org>  Tue, 06 Aug 2024 16:02:54 +0200
+
 roundcube (1.6.5+dfsg-1+deb12u2) bookworm-security; urgency=high
 
   * Fix CVE-2024-37384: Cross-site scripting (XSS) vulnerability in handling
diff -Nru roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42008.patch roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42008.patch
--- roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42008.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42008.patch	2024-08-06 16:02:54.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/89c8fe9ae9318c015807fbcbf7e39555fb30885d
+Bug-Debian: https://bugs.debian.org/1077969
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-42008
+---
+ program/actions/mail/get.php           |  5 +++++
+ program/lib/Roundcube/rcube_output.php | 20 +++++++++++++++-----
+ 2 files changed, 20 insertions(+), 5 deletions(-)
+
+diff --git a/program/actions/mail/get.php b/program/actions/mail/get.php
+index 4cf4797..335f417 100644
+--- a/program/actions/mail/get.php
++++ b/program/actions/mail/get.php
+@@ -259,6 +259,11 @@ class rcmail_action_mail_get extends rcmail_action_mail_index
+                             $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'");
+                     }
+                 }
+ 
+diff --git a/program/lib/Roundcube/rcube_output.php b/program/lib/Roundcube/rcube_output.php
+index 8aa2bda..0a8b664 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 -Nru roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42009.patch roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42009.patch
--- roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42009.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42009.patch	2024-08-06 16:02:54.000000000 +0200
@@ -0,0 +1,624 @@
+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/68af7c864a36e1941764238dac440ab0d99a8d26
+Bug-Debian: https://bugs.debian.org/1077969
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-42009
+---
+ program/actions/mail/compose.php        |  36 ++----
+ program/actions/mail/index.php          | 195 +++++++++++++-------------------
+ program/actions/mail/show.php           |   4 -
+ program/actions/settings/index.php      |   9 +-
+ program/lib/Roundcube/rcube_washtml.php |  50 ++++----
+ tests/Actions/Mail/Index.php            |  71 +++++++-----
+ 6 files changed, 169 insertions(+), 196 deletions(-)
+
+diff --git a/program/actions/mail/compose.php b/program/actions/mail/compose.php
+index 1d5d33f..0494084 100644
+--- a/program/actions/mail/compose.php
++++ b/program/actions/mail/compose.php
+@@ -396,7 +396,7 @@ class rcmail_action_mail_compose extends rcmail_action_mail_index
+         // 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              = ['safe' => false, 'inline_html' => true];
++                $wash_params              = ['safe' => false];
+                 $COMPOSE['param']['body'] = self::prepare_html_body($COMPOSE['param']['body'], $wash_params);
+             }
+         }
+@@ -985,39 +985,25 @@ class rcmail_action_mail_compose extends rcmail_action_mail_index
+         static $part_no;
+ 
+         // Set attributes of the part container
+-        $container_id     = self::$COMPOSE['mode'] . 'body' . (++$part_no);
+-        $container_attrib = ['id' => $container_id];
+-        $body_args        = [
+-            'safe'             => self::$MESSAGE->is_safe,
+-            'plain'            => false,
+-            'css_prefix'       => 'v' . $part_no,
++        $container_id = self::$COMPOSE['mode'] . 'body' . (++$part_no);
++        $wash_params += [
++            'safe'         => self::$MESSAGE->is_safe,
++            'css_prefix'   => 'v' . $part_no,
++            'add_comments' => false,
+         ];
+ 
+-        // remove comments (produced by washtml)
+-        $replace = ['/<!--[^>]+-->/' => ''];
+-
+         if (self::$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'] = ['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 = self::wash_html($body, $wash_params + $body_args, self::$CID_MAP);
+-        $body = preg_replace(array_keys($replace), array_values($replace), $body);
+-        $body = self::html4inline($body, $body_args);
+-
+-        if (self::$COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT) {
+-            $body = html::div($container_attrib, $body);
+-        }
+-
+-        return $body;
++        return self::wash_html($body, $wash_params, self::$CID_MAP);
+     }
+ 
+     // Removes signature from the message body
+diff --git a/program/actions/mail/index.php b/program/actions/mail/index.php
+index e075359..9c54955 100644
+--- a/program/actions/mail/index.php
++++ b/program/actions/mail/index.php
+@@ -35,6 +35,7 @@ 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 = [];
+ 
+     /**
+      * Request handler.
+@@ -936,13 +937,17 @@ class rcmail_action_mail_index extends rcmail_action
+         // clean HTML with washtml by Frederic Motte
+         $wash_opts = [
+             'show_washed'   => false,
++            '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' => ['body'],
+             'css_prefix'    => $p['css_prefix'],
++            'ignore_elements' => $p['ignore_elements'] ?? [],
++            // internal configuration
+             'container_id'  => $p['container_id'],
++            'body_class'    => $p['body_class'] ?? '',
+         ];
+ 
+         if (empty($p['inline_html'])) {
+@@ -963,11 +968,25 @@ class rcmail_action_mail_index extends rcmail_action
+         // initialize HTML washer
+         $washer = new rcube_washtml($wash_opts);
+ 
++        self::$wash_html_body_attrs = [];
++
++        if (!empty($p['inline_html'])) {
++            $washer->add_callback('body', 'rcmail_action_mail_index::washtml_callback');
++
++            if ($wash_opts['body_class']) {
++                self::$wash_html_body_attrs['class'] = $wash_opts['body_class'];
++            }
++
++            if ($wash_opts['container_id']) {
++                self::$wash_html_body_attrs['id'] = $wash_opts['container_id'];
++            }
++        }
++
+         if (empty($p['skip_washer_form_callback'])) {
+             $washer->add_callback('form', 'rcmail_action_mail_index::washtml_callback');
+         }
+ 
+-        // allow CSS styles, will be sanitized by rcmail_washtml_callback()
++        // allow CSS styles, will be sanitized by self::washtml_callback()
+         if (empty($p['skip_washer_style_callback'])) {
+             $washer->add_callback('style', 'rcmail_action_mail_index::washtml_callback');
+         }
+@@ -985,6 +1004,11 @@ class rcmail_action_mail_index extends rcmail_action
+         $html = $washer->wash($html);
+         self::$REMOTE_OBJECTS = $washer->extlinks;
+ 
++        // There was no <body>, but a wrapper element is required
++        if (!empty($p['inline_html']) && !empty(self::$wash_html_body_attrs)) {
++            $html = html::tag('div', self::$wash_html_body_attrs, $html);
++        }
++
+         return $html;
+     }
+ 
+@@ -1113,9 +1137,64 @@ class rcmail_action_mail_index extends rcmail_action
+                     $washtml->extlinks = true;
+                 }
+                 else {
+-                    $out = html::tag('style', ['type' => 'text/css'], $decoded);
++                    $out = $decoded;
++                }
++            }
++
++            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', ['type' => 'text/css'], $out);
++            }
++
++            break;
++
++        case 'body':
++            $style = [];
++            $attrs = self::$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);
++            self::$wash_html_body_attrs = [];
++            break;
+         }
+ 
+         return $out;
+@@ -1179,118 +1258,6 @@ class rcmail_action_mail_index extends rcmail_action
+         }
+     }
+ 
+-    /**
+-     * Modify a HTML message that it can be displayed inside a HTML page
+-     */
+-    public static function html4inline($body, &$args)
+-    {
+-        $last_pos = 0;
+-        $is_safe  = !empty($args['safe']);
+-        $prefix   = $args['css_prefix'] ?? null;
+-        $cont_id  = trim(
+-            (!empty($args['container_id']) ? $args['container_id'] : '')
+-            . (!empty($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, $is_safe, $prefix);
+-
+-            $body     = substr_replace($body, $styles, $pos, $len);
+-            $last_pos = $pos2 + strlen($styles) - $len;
+-        }
+-
+-        $replace = [
+-            // 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 = [];
+-            $attrs = $m[0];
+-
+-            // Get bgcolor, we'll set it as background-color of the message container
+-            if (!empty($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 (!empty($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 (!empty($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 (!empty($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 self::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/actions/mail/show.php b/program/actions/mail/show.php
+index ff798b8..14af62f 100644
+--- a/program/actions/mail/show.php
++++ b/program/actions/mail/show.php
+@@ -742,10 +742,6 @@ class rcmail_action_mail_show extends rcmail_action_mail_index
+                         $rcmail->output->set_env('is_pgp_content', '#' . $container_id);
+                     }
+ 
+-                    if ($part->ctype_secondary == 'html') {
+-                        $body = self::html4inline($body, $body_args);
+-                    }
+-
+                     $out .= html::div($body_args['container_attrib'], $plugin['prefix'] . $body);
+                 }
+             }
+diff --git a/program/actions/settings/index.php b/program/actions/settings/index.php
+index fdc3471..8e67e19 100644
+--- a/program/actions/settings/index.php
++++ b/program/actions/settings/index.php
+@@ -1774,6 +1774,8 @@ class rcmail_action_settings_index extends rcmail_action
+             'charset'       => RCUBE_CHARSET,
+             'html_elements' => ['body', 'link'],
+             'html_attribs'  => ['rel', 'type'],
++            'ignore_elements' => ['body'],
++            'add_comments'  => false,
+         ];
+ 
+         // initialize HTML washer
+@@ -1782,11 +1784,6 @@ class rcmail_action_settings_index extends rcmail_action
+         // 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(['/<!--[^>]+-->/', '/<\/?body>/'], '', $html);
+-
+-        return $html;
++        return $washer->wash($html);
+     }
+ }
+diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php
+index 3041842..a07b2d2 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 = [];
++    private $config = [
++        'add_comments' => true,
++        'allow_remote' => false,
++        'base_url' => '',
++        'charset' => RCUBE_CHARSET,
++        'cid_map' => [],
++        'show_washed' => true,
++    ];
+ 
+     /** @var array Registered callback functions for tags */
+     private $handlers = [];
+@@ -225,11 +232,7 @@ class rcube_washtml
+ 
+         unset($p['html_elements'], $p['html_attribs'], $p['ignore_elements'], $p['void_elements'], $p['css_prefix']);
+ 
+-        $this->config = $p + ['show_washed' => true, 'allow_remote' => false, 'cid_map' => [], 'base_url' => ''];
+-
+-        if (!isset($this->config['charset'])) {
+-            $this->config['charset'] = RCUBE_CHARSET;
+-        }
++        $this->config = array_merge($this->config, $p);
+     }
+ 
+     /**
+@@ -579,7 +582,7 @@ class rcube_washtml
+                 );
+             }
+ 
+-            return '<!-- ignored -->';
++            return $this->config['add_comments'] ? '<!-- ignored -->' : '';
+         }
+ 
+         $node = $node->firstChild;
+@@ -593,7 +596,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;
+                     }
+ 
+@@ -603,7 +608,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;
+                 }
+ 
+@@ -614,13 +621,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'])
+                                 );
+@@ -631,20 +638,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/tests/Actions/Mail/Index.php b/tests/Actions/Mail/Index.php
+index f719793..b3ae049 100644
+--- a/tests/Actions/Mail/Index.php
++++ b/tests/Actions/Mail/Index.php
+@@ -327,12 +327,11 @@ class Actions_Mail_Index extends ActionTestCase
+         $part = $this->get_html_part('src/htmlbody.txt');
+         $part->replaces = ['ex1.jpg' => 'part_1.2.jpg', 'ex2.jpg' => 'part_1.2.jpg'];
+ 
+-        $params = ['container_id' => 'foo'];
++        $params = ['container_id' => 'foo', 'safe' => false];
+ 
+         // render HTML in normal mode
+-        $body = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => false]);
+-        $html = rcmail_action_mail_index::html4inline($body, $params);
+-
++        $html = \rcmail_action_mail_index::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");
+@@ -344,8 +343,8 @@ class Actions_Mail_Index extends ActionTestCase
+ //        $this->assertTrue($GLOBALS['REMOTE_OBJECTS'], "Remote object detected");
+ 
+         // render HTML in safe mode
+-        $body = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => true]);
+-        $html = rcmail_action_mail_index::html4inline($body, $params);
++        $params['safe'] = true;
++        $html = \rcmail_action_mail_index::print_body($part->body, $part, $params);
+ 
+         $this->assertMatchesRegularExpression('/<style [^>]+>/', $html, "Allow styles in safe mode");
+         $this->assertMatchesRegularExpression('#src="http://evilsite.net/mailings/ex3.jpg"#', $html, "Allow external images in HTML (safe mode)");
+@@ -362,15 +361,12 @@ class Actions_Mail_Index extends ActionTestCase
+         $this->initOutput(rcmail_action::MODE_HTTP, 'mail', '');
+ 
+         $part   = $this->get_html_part('src/htmlxss.txt');
+-        $washed = rcmail_action_mail_index::print_body($part->body, $part, ['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 = ['container_id' => 'foo'];
+-        $html   = rcmail_action_mail_index::html4inline($washed, $params);
++        $params = ['container_id' => 'foo', 'safe' => true];
++        $html = \rcmail_action_mail_index::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");
+     }
+@@ -385,8 +381,7 @@ class Actions_Mail_Index extends ActionTestCase
+ 
+         $part   = $this->get_html_part('src/BID-26800.txt');
+         $params = ['container_id' => 'dabody', 'safe' => true];
+-        $body   = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => true]);
+-        $washed = rcmail_action_mail_index::html4inline($body, $params);
++        $washed = \rcmail_action_mail_index::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");
+@@ -408,20 +403,40 @@ class Actions_Mail_Index extends ActionTestCase
+         $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 = ['safe' => true, 'add_comments' => false];
++        $washed = \rcmail_action_mail_index::print_body($part->body, $part, $params);
++
++        $this->assertSame(str_replace('body', 'div', $part->body), $washed);
++
++        $params['inline_html'] = false;
++        $washed = \rcmail_action_mail_index::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 = ['container_id' => 'foo'];
+-        $html   = rcmail_action_mail_index::html4inline($html, $params);
++        $html = '<body background="http://test.com/image" bgcolor="#fff" style="font-size: 11px" text="#000"><p>test</p></body>';
++        $params = ['container_id' => 'foo', 'add_comments' => false, 'safe' => false];
++        $washed = \rcmail_action_mail_index::wash_html($html, $params, []);
++
++        $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>', $washed);
+ 
+-        $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)");
++        $params['safe'] = true;
++        $washed = \rcmail_action_mail_index::wash_html($html, $params, []);
++ 
++        $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>', $washed);
+     }
+ 
+     /**
+@@ -447,6 +462,7 @@ class Actions_Mail_Index extends ActionTestCase
+ 
+         $meta = '<meta charset="'.RCUBE_CHARSET.'" />';
+         $args = [
++            'inline_html' => false,
+             'html_elements' => ['html', 'body', 'meta', 'head'],
+             'html_attribs'  => ['charset'],
+         ];
+@@ -515,11 +531,10 @@ class Actions_Mail_Index extends ActionTestCase
+         $this->initOutput(rcmail_action::MODE_HTTP, 'mail', '');
+ 
+         $part   = $this->get_html_part('src/mailto.txt');
+-        $params = ['container_id' => 'foo'];
++        $params = ['container_id' => 'foo', 'safe' => false];
+ 
+         // render HTML in normal mode
+-        $body = rcmail_action_mail_index::print_body($part->body, $part, ['safe' => false]);
+-        $html = rcmail_action_mail_index::html4inline($body, $params);
++        $html = \rcmail_action_mail_index::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.6.5+dfsg/debian/patches/CVE-2024-42010.patch roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42010.patch
--- roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42010.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.6.5+dfsg/debian/patches/CVE-2024-42010.patch	2024-08-06 16:02:54.000000000 +0200
@@ -0,0 +1,408 @@
+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/602d0f566eb39b6dcb739ad78323ec434a3b92ce
+Bug-Debian: https://bugs.debian.org/1077969
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2024-42010
+---
+ program/lib/Roundcube/rcube_utils.php   | 157 ++++++++++++++++++++++++--------
+ program/lib/Roundcube/rcube_washtml.php |  80 ++--------------
+ tests/Framework/Utils.php               |  52 ++++++++---
+ tests/Framework/Washtml.php             |   2 +-
+ 4 files changed, 166 insertions(+), 125 deletions(-)
+
+diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php
+index 2724c01..6811553 100644
+--- a/program/lib/Roundcube/rcube_utils.php
++++ b/program/lib/Roundcube/rcube_utils.php
+@@ -416,23 +416,41 @@ 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! */';
+         }
+ 
+-        $strict_url_regexp = '!url\s*\(\s*["\']?(https?:)//[a-z0-9/._+-]+["\']?\s*\)!Uims';
++        // 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! */';
++        }
+ 
+         // 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))) {
+             $nested = strpos($source, '{', $pos+1);
+@@ -441,36 +459,9 @@ class rcube_utils
+             }
+             $length = $pos2 - $pos - 1;
+             $styles = substr($source, $pos+1, $length);
+-            $output = '';
+-
+-            // check every css rule in the style block...
+-            foreach (self::parse_css_block($styles) as $rule) {
+-                // Remove 'page' attributes (#7604)
+-                if ($rule[0] == 'page') {
+-                    continue;
+-                }
+-
+-                // Convert position:fixed to position:absolute (#5264)
+-                if ($rule[0] == 'position' && strcasecmp($rule[1], 'fixed') === 0) {
+-                    $rule[1] = 'absolute';
+-                }
+-                else if ($allow_remote) {
+-                    $stripped = preg_replace('/[^a-z\(:;]/i', '', $rule[1]);
+-
+-                    // allow data:image and strict url() values only
+-                    if (
+-                        stripos($stripped, 'url(') !== false
+-                        && stripos($stripped, 'url(data:image') === false
+-                        && !preg_match($strict_url_regexp, $rule[1])
+-                    ) {
+-                        $rule[1] = '/* evil! */';
+-                    }
+-                }
+-
+-                $output .= sprintf(" %s: %s;", $rule[0] , $rule[1]);
+-            }
++            $styles = self::sanitize_css_block($styles, $url_callback);
+ 
+-            $key      = $replacements->add($output . ' ');
++            $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));
+@@ -518,6 +509,63 @@ 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 = [];
++
++        // 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.
+@@ -590,6 +638,41 @@ class rcube_utils
+         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 = [];
++        $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 a07b2d2..e9dcea4 100644
+--- a/program/lib/Roundcube/rcube_washtml.php
++++ b/program/lib/Roundcube/rcube_washtml.php
+@@ -257,48 +257,19 @@ class rcube_washtml
+     {
+         $result = [];
+ 
+-        // 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 (rcube_utils::parse_css_block($style) as $rule) {
+-            $cssid = $rule[0];
+-            $value = '';
+-
+-            foreach ($this->explode_style($rule[1]) 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 . ': ' . trim($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), ';');
+     }
+ 
+     /**
+@@ -990,39 +961,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)
+-    {
+-        $result = [];
+-        $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;
+-                }
+-                else if (!$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;
+-    }
+ }
+diff --git a/tests/Framework/Utils.php b/tests/Framework/Utils.php
+index 7ea6ffa..4cd2750 100644
+--- a/tests/Framework/Utils.php
++++ b/tests/Framework/Utils.php
+@@ -217,26 +217,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');
+@@ -278,9 +300,9 @@ class Framework_Utils extends PHPUnit\Framework\TestCase
+         $mod = rcube_utils::mod_css_styles($style, 'rcmbody', true);
+         $this->assertSame("#rcmbody { color: red; }", $mod);
+ 
+-        $style = "body { background:url(alert('URL!') ) }";
++        $style = "body { background:url(alert('URL!')); }";
+         $mod = rcube_utils::mod_css_styles($style, 'rcmbody', true);
+-        $this->assertSame("#rcmbody { background: /* evil! */; }", $mod);
++        $this->assertSame("#rcmbody {}", $mod);
+     }
+ 
+     /**
+diff --git a/tests/Framework/Washtml.php b/tests/Framework/Washtml.php
+index 6bfbc14..4fdae1a 100644
+--- a/tests/Framework/Washtml.php
++++ b/tests/Framework/Washtml.php
+@@ -289,7 +289,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.6.5+dfsg/debian/patches/Fix-bug-where-an-unhandled-exception-was-caused-by-an-inv.patch roundcube-1.6.5+dfsg/debian/patches/Fix-bug-where-an-unhandled-exception-was-caused-by-an-inv.patch
--- roundcube-1.6.5+dfsg/debian/patches/Fix-bug-where-an-unhandled-exception-was-caused-by-an-inv.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.6.5+dfsg/debian/patches/Fix-bug-where-an-unhandled-exception-was-caused-by-an-inv.patch	2024-08-06 16:02:54.000000000 +0200
@@ -0,0 +1,223 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Sun, 21 Jul 2024 14:23:28 +0200
+Subject: Fix bug where an unhandled exception was caused by an invalid image
+ attachment
+
+GD functions may throw ValueError in some cases since PHP 8.0.
+We wrap them in try/catch blocks.
+
+Origin: https://github.com/roundcube/roundcubemail/commit/9d9f4d6926e16e9acd46231ee6d03695d058565a
+Bug: https://github.com/roundcube/roundcubemail/issues/9475
+---
+ program/lib/Roundcube/rcube_image.php | 179 +++++++++++++++++-----------------
+ 1 file changed, 88 insertions(+), 91 deletions(-)
+
+diff --git a/program/lib/Roundcube/rcube_image.php b/program/lib/Roundcube/rcube_image.php
+index 1b980c8..d034c17 100644
+--- a/program/lib/Roundcube/rcube_image.php
++++ b/program/lib/Roundcube/rcube_image.php
+@@ -217,87 +217,85 @@ class rcube_image
+ 
+         // use GD extension
+         if ($props['gd_type'] && $props['width'] > 0 && $props['height'] > 0) {
+-            if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
+-                $image = @imagecreatefromjpeg($this->image_file);
+-                $type  = 'jpg';
+-            }
+-            else if ($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
+-                $image = @imagecreatefromgif($this->image_file);
+-                $type  = 'gif';
+-            }
+-            else if ($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
+-                $image = @imagecreatefrompng($this->image_file);
+-                $type  = 'png';
+-            }
+-            else {
+-                // @TODO: print error to the log?
+-                return false;
+-            }
++            try {
++                if ($props['gd_type'] == \IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
++                    $image = imagecreatefromjpeg($this->image_file);
++                    $type = 'jpg';
++                } elseif ($props['gd_type'] == \IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
++                    $image = imagecreatefromgif($this->image_file);
++                    $type = 'gif';
++                } elseif ($props['gd_type'] == \IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
++                    $image = imagecreatefrompng($this->image_file);
++                    $type = 'png';
++                } else {
++                    // @TODO: print error to the log?
++                    return false;
++                }
+ 
+-            if ($image === false) {
+-                return false;
+-            }
++                if ($image === false) {
++                    return false;
++                }
+ 
+-            $scale = $size / max($props['width'], $props['height']);
++                $scale = $size / max($props['width'], $props['height']);
+ 
+-            // Imagemagick resize is implemented in shrinking mode (see -resize argument above)
+-            // we do the same here, if an image is smaller than specified size
+-            // we do nothing but copy original file to destination file
+-            if ($scale >= 1) {
+-                $result = $this->image_file == $filename || copy($this->image_file, $filename);
+-            }
+-            else {
+-                $width     = intval($props['width']  * $scale);
+-                $height    = intval($props['height'] * $scale);
+-                $new_image = imagecreatetruecolor($width, $height);
++                // Imagemagick resize is implemented in shrinking mode (see -resize argument above)
++                // we do the same here, if an image is smaller than specified size
++                // we do nothing but copy original file to destination file
++                if ($scale >= 1) {
++                    $result = $this->image_file == $filename || copy($this->image_file, $filename);
++                } else {
++                    $width = intval($props['width'] * $scale);
++                    $height = intval($props['height'] * $scale);
++                    $new_image = imagecreatetruecolor($width, $height);
+ 
+-                if ($new_image === false) {
+-                    return false;
+-                }
++                    if ($new_image === false) {
++                        return false;
++                    }
+ 
+-                // Fix transparency of gif/png image
+-                if ($props['gd_type'] != IMAGETYPE_JPEG) {
+-                    imagealphablending($new_image, false);
+-                    imagesavealpha($new_image, true);
+-                    $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
+-                    imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
+-                }
++                    // Fix transparency of gif/png image
++                    if ($props['gd_type'] != \IMAGETYPE_JPEG) {
++                        imagealphablending($new_image, false);
++                        imagesavealpha($new_image, true);
++                        $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
++                        imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
++                    }
+ 
+-                imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
+-                $image = $new_image;
+-
+-                // fix orientation of image if EXIF data exists and specifies orientation (GD strips the EXIF data)
+-                if ($this->image_file && $type == 'jpg' && function_exists('exif_read_data')) {
+-                    $exif = @exif_read_data($this->image_file);
+-                    if ($exif && !empty($exif['Orientation'])) {
+-                        switch ($exif['Orientation']) {
+-                            case 3:
+-                                $image = imagerotate($image, 180, 0);
+-                                break;
+-                            case 6:
+-                                $image = imagerotate($image, -90, 0);
+-                                break;
+-                            case 8:
+-                                $image = imagerotate($image, 90, 0);
+-                                break;
++                    imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
++                    $image = $new_image;
++
++                    // fix orientation of image if EXIF data exists and specifies orientation (GD strips the EXIF data)
++                    if ($this->image_file && $type == 'jpg' && function_exists('exif_read_data')) {
++                        $exif = @exif_read_data($this->image_file);
++                        if ($exif && !empty($exif['Orientation'])) {
++                            switch ($exif['Orientation']) {
++                                case 3:
++                                    $image = imagerotate($image, 180, 0);
++                                    break;
++                                case 6:
++                                    $image = imagerotate($image, -90, 0);
++                                    break;
++                                case 8:
++                                    $image = imagerotate($image, 90, 0);
++                                    break;
++                            }
+                         }
+                     }
+-                }
+ 
+-                if ($props['gd_type'] == IMAGETYPE_JPEG) {
+-                    $result = imagejpeg($image, $filename, 75);
+-                }
+-                elseif($props['gd_type'] == IMAGETYPE_GIF) {
+-                    $result = imagegif($image, $filename);
+-                }
+-                elseif($props['gd_type'] == IMAGETYPE_PNG) {
+-                    $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
++                    if ($props['gd_type'] == \IMAGETYPE_JPEG) {
++                        $result = imagejpeg($image, $filename, 75);
++                    } elseif ($props['gd_type'] == \IMAGETYPE_GIF) {
++                        $result = imagegif($image, $filename);
++                    } elseif ($props['gd_type'] == \IMAGETYPE_PNG) {
++                        $result = imagepng($image, $filename, 6, \PNG_ALL_FILTERS);
++                    }
+                 }
+-            }
+ 
+-            if ($result) {
+-                @chmod($filename, 0600);
+-                return $type;
++                if ($result) {
++                    @chmod($filename, 0600);
++                    return $type;
++                }
++            } catch (Throwable $e) {
++                rcube::raise_error($e, true, false);
+             }
+         }
+ 
+@@ -371,28 +369,27 @@ class rcube_image
+         }
+ 
+         if ($props['gd_type']) {
+-            if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
+-                $image = imagecreatefromjpeg($this->image_file);
+-            }
+-            else if ($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
+-                $image = imagecreatefromgif($this->image_file);
+-            }
+-            else if ($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
+-                $image = imagecreatefrompng($this->image_file);
+-            }
+-            else {
+-                // @TODO: print error to the log?
+-                return false;
+-            }
++            try {
++                if ($props['gd_type'] == \IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
++                    $image = imagecreatefromjpeg($this->image_file);
++                } elseif ($props['gd_type'] == \IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
++                    $image = imagecreatefromgif($this->image_file);
++                } elseif ($props['gd_type'] == \IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
++                    $image = imagecreatefrompng($this->image_file);
++                } else {
++                    // @TODO: print error to the log?
++                    return false;
++                }
+ 
+-            if ($type == self::TYPE_JPG) {
+-                $result = imagejpeg($image, $filename, 75);
+-            }
+-            else if ($type == self::TYPE_GIF) {
+-                $result = imagegif($image, $filename);
+-            }
+-            else if ($type == self::TYPE_PNG) {
+-                $result = imagepng($image, $filename, 6, PNG_ALL_FILTERS);
++                if ($type == self::TYPE_JPG) {
++                    $result = imagejpeg($image, $filename, 75);
++                } elseif ($type == self::TYPE_GIF) {
++                    $result = imagegif($image, $filename);
++                } elseif ($type == self::TYPE_PNG) {
++                    $result = imagepng($image, $filename, 6, \PNG_ALL_FILTERS);
++                }
++            } catch (Throwable $e) {
++                rcube::raise_error($e, true, false);
+             }
+ 
+             if (!empty($result)) {
diff -Nru roundcube-1.6.5+dfsg/debian/patches/Fix-bug-where-imap_conn_option-s-socket-was-ignored.patch roundcube-1.6.5+dfsg/debian/patches/Fix-bug-where-imap_conn_option-s-socket-was-ignored.patch
--- roundcube-1.6.5+dfsg/debian/patches/Fix-bug-where-imap_conn_option-s-socket-was-ignored.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.6.5+dfsg/debian/patches/Fix-bug-where-imap_conn_option-s-socket-was-ignored.patch	2024-08-06 16:02:54.000000000 +0200
@@ -0,0 +1,25 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Fri, 2 Aug 2024 12:16:01 +0200
+Subject: Fix bug where imap_conn_option's 'socket' was ignored
+
+Origin: https://github.com/roundcube/roundcubemail/commit/b5ed0e49464ecee70756ad6d1b96d38279b3916e
+Bug: https://github.com/roundcube/roundcubemail/issues/9566
+---
+ program/lib/Roundcube/rcube_imap_generic.php | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php
+index afe7c23..f10d823 100644
+--- a/program/lib/Roundcube/rcube_imap_generic.php
++++ b/program/lib/Roundcube/rcube_imap_generic.php
+@@ -1054,8 +1054,8 @@ class rcube_imap_generic
+         }
+ 
+         if (!empty($this->prefs['socket_options'])) {
+-            $options  = array_intersect_key($this->prefs['socket_options'], ['ssl' => 1]);
+-            $context  = stream_context_create($options);
++            $options = array_intersect_key($this->prefs['socket_options'], ['ssl' => 1, 'socket' => 1]);
++            $context = stream_context_create($options);
+             $this->fp = stream_socket_client($host . ':' . $port, $errno, $errstr,
+                 $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context);
+         }
diff -Nru roundcube-1.6.5+dfsg/debian/patches/Fix-fatal-error-when-parsing-some-TNEF-attachments.patch roundcube-1.6.5+dfsg/debian/patches/Fix-fatal-error-when-parsing-some-TNEF-attachments.patch
--- roundcube-1.6.5+dfsg/debian/patches/Fix-fatal-error-when-parsing-some-TNEF-attachments.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.6.5+dfsg/debian/patches/Fix-fatal-error-when-parsing-some-TNEF-attachments.patch	2024-08-06 16:02:54.000000000 +0200
@@ -0,0 +1,128 @@
+From: Aleksander Machniak <alec at alec.pl>
+Date: Sun, 2 Jun 2024 15:13:42 +0200
+Subject: Fix fatal error when parsing some TNEF attachments
+
+Origin: https://github.com/roundcube/roundcubemail/commit/22d403d5fdea1846319389d3d65ef60726434712
+Bug: https://github.com/roundcube/roundcubemail/issues/9462
+---
+ program/lib/Roundcube/rcube_tnef_decoder.php |  3 +-
+ tests/Framework/Message.php                  | 87 ++++++++++++++++++++++++++++
+ 2 files changed, 89 insertions(+), 1 deletion(-)
+
+diff --git a/program/lib/Roundcube/rcube_tnef_decoder.php b/program/lib/Roundcube/rcube_tnef_decoder.php
+index 092e6c5..8308303 100644
+--- a/program/lib/Roundcube/rcube_tnef_decoder.php
++++ b/program/lib/Roundcube/rcube_tnef_decoder.php
+@@ -162,7 +162,7 @@ class rcube_tnef_decoder
+         }
+ 
+         // Return the message body as HTML
+-        if ($message && $as_html) {
++        if ($as_html) {
+             // HTML body
+             if (!empty($message['size']) && $message['subtype'] == 'html') {
+                 $message = $message['stream'];
+@@ -180,6 +180,7 @@ class rcube_tnef_decoder
+                 }
+                 catch (Exception $e) {
+                     // ignore the body
++                    $message = null;
+                     rcube::raise_error([
+                             'file' => __FILE__,
+                             'line' => __LINE__,
+diff --git a/tests/Framework/Message.php b/tests/Framework/Message.php
+index 16b4661..3e40e93 100644
+--- a/tests/Framework/Message.php
++++ b/tests/Framework/Message.php
+@@ -18,4 +18,91 @@ class Framework_Message extends PHPUnit\Framework\TestCase
+ 
+         $this->assertSame('test', $result);
+     }
++
++    /**
++     * Test tnef_decode() method
++     */
++    public function test_tnef_decode()
++    {
++        $message = new rcube_message_test(123);
++        $part = new rcube_message_part();
++        $part->mime_id = 1;
++
++        $message->set_part_body(1, '');
++        $result = $message->tnef_decode($part);
++
++        $this->assertSame([], $result);
++
++        $message->set_part_body(1, file_get_contents(TESTS_DIR . 'src/body.tnef'));
++        $result = $message->tnef_decode($part);
++
++        $this->assertCount(1, $result);
++        $this->assertInstanceOf('rcube_message_part', $result[0]);
++        $this->assertSame('winmail.1.html', $result[0]->mime_id);
++        $this->assertSame('text/html', $result[0]->mimetype);
++        $this->assertSame(5360, $result[0]->size);
++        $this->assertStringStartsWith('<!DOCTYPE HTML', $result[0]->body);
++        $this->assertSame([], $result[0]->parts);
++
++        $message->set_part_body(1, file_get_contents(TESTS_DIR . 'src/one-file.tnef'));
++        $result = $message->tnef_decode($part);
++
++        $this->assertCount(1, $result);
++        $this->assertInstanceOf('rcube_message_part', $result[0]);
++        $this->assertSame('winmail.1.0', $result[0]->mime_id);
++        $this->assertSame('application/octet-stream', $result[0]->mimetype);
++        $this->assertSame(244, $result[0]->size);
++        $this->assertStringContainsString(' Authors of', $result[0]->body);
++        $this->assertSame([], $result[0]->parts);
++    }
++
++    /**
++     * Test uu_decode() method
++     */
++    public function test_uu_decode()
++    {
++        $message = new rcube_message_test(123);
++        $part = new rcube_message_part();
++        $part->mime_id = 1;
++
++        $message->set_part_body(1, '');
++        $result = $message->uu_decode($part);
++
++        $this->assertSame([], $result);
++
++        $content = "begin 644 /dev/stdout\n" . convert_uuencode('test') . "end";
++        $message->set_part_body(1, $content);
++
++        $result = $message->uu_decode($part);
++
++        $this->assertCount(1, $result);
++        $this->assertInstanceOf('rcube_message_part', $result[0]);
++        $this->assertSame('uu.1.0', $result[0]->mime_id);
++        $this->assertSame('text/plain', $result[0]->mimetype);
++        $this->assertSame(4, $result[0]->size);
++        $this->assertSame('test', $result[0]->body);
++        $this->assertSame([], $result[0]->parts);
++    }
++}
++
++/**
++ * rcube_message wrapper for easier testing (without accessing IMAP)
++ */
++class rcube_message_test extends rcube_message
++{
++    private $part_bodies = [];
++
++    public function __construct($uid, $folder = null, $is_safe = false)
++    {
++    }
++
++    public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null)
++    {
++        return $this->part_bodies[$mime_id] ?? null;
++    }
++
++    public function set_part_body($mime_id, $body)
++    {
++        $this->part_bodies[$mime_id] = $body;
++    }
+ }
diff -Nru roundcube-1.6.5+dfsg/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch roundcube-1.6.5+dfsg/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch
--- roundcube-1.6.5+dfsg/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch	1970-01-01 01:00:00.000000000 +0100
+++ roundcube-1.6.5+dfsg/debian/patches/Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch	2024-08-06 16:02:54.000000000 +0200
@@ -0,0 +1,54 @@
+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/Script.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 05a53a2..3c9d7fc 100644
+--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
++++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_script.php
+@@ -1353,9 +1353,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/Script.php b/plugins/managesieve/tests/Script.php
+index 41982d1..f9ea3f4 100644
+--- a/plugins/managesieve/tests/Script.php
++++ b/plugins/managesieve/tests/Script.php
+@@ -59,6 +59,20 @@ class Managesieve_Script 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);
++    }
++
+     function data_tokenizer()
+     {
+         return [
diff -Nru roundcube-1.6.5+dfsg/debian/patches/series roundcube-1.6.5+dfsg/debian/patches/series
--- roundcube-1.6.5+dfsg/debian/patches/series	2024-06-17 03:15:26.000000000 +0200
+++ roundcube-1.6.5+dfsg/debian/patches/series	2024-08-06 16:02:54.000000000 +0200
@@ -18,3 +18,10 @@
 fix-upstream-test-suite.patch
 CVE-2024-37384.patch
 CVE-2024-37383.patch
+Fix-fatal-error-when-parsing-some-TNEF-attachments.patch
+Fix-bug-where-an-unhandled-exception-was-caused-by-an-inv.patch
+Fix-infinite-loop-when-parsing-malformed-Sieve-script.patch
+Fix-bug-where-imap_conn_option-s-socket-was-ignored.patch
+CVE-2024-42009.patch
+CVE-2024-42008.patch
+CVE-2024-42010.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/20240806/09cd3762/attachment-0001.sig>


More information about the Pkg-roundcube-maintainers mailing list