On Wed, 21 Oct 2026 at 05:49:58 +0200, Guilhem Moulin wrote: > On Mon, 19 Oct 2026 at 16:22:07 +0200, Guilhem Moulin wrote: >> Roundcube webmail upstream has just released 1.6.20 [0] which fixes a >> new batch of security vulnerabilities: > > As for the previous uploads, I suggest to follow 1.6.x (upstream's > current LTS release) for trixie-security. The upstream diff [0] is > targeted anyway. Several of the issues, in particular the SSRF, XSS and > remote-content blocker bypass, have test coverage. > > Tested debdiff attached. Debusine result: > https://debusine.debian.net/debian/developers/work-request/1508582/
Oops, now with the right recipient and the attachment. -- Guilhem.
diffstat for roundcube-1.6.19+dfsg roundcube-1.6.20+dfsg CHANGELOG.md | 23 + debian/changelog | 38 ++ debian/patches/Avoid-dependency-on-new-package-mlocati-ip-lib.patch | 22 - debian/patches/Fix-FTBFS-with-phpunit-11.patch | 148 +++++----- index.php | 10 plugins/markasjunk/markasjunk.php | 2 plugins/password/drivers/directadmin.php | 2 plugins/password/drivers/modoboa.php | 29 + plugins/password/password.php | 2 plugins/squirrelmail_usercopy/squirrelmail_usercopy.php | 5 program/actions/contacts/upload_photo.php | 2 program/actions/mail/compose.php | 6 program/actions/mail/headers.php | 3 program/actions/mail/viewsource.php | 3 program/actions/utils/modcss.php | 23 + program/include/rcmail.php | 25 + program/include/rcmail_attachment_handler.php | 3 program/include/rcmail_sendmail.php | 14 program/lib/Roundcube/rcube_image.php | 5 program/lib/Roundcube/rcube_imap_generic.php | 4 program/lib/Roundcube/rcube_mime.php | 3 program/lib/Roundcube/rcube_session.php | 9 program/lib/Roundcube/rcube_user.php | 4 program/lib/Roundcube/rcube_utils.php | 10 program/lib/Roundcube/rcube_washtml.php | 73 ++++ public_html/plugins/markasjunk/markasjunk.php | 2 public_html/plugins/password/drivers/directadmin.php | 2 public_html/plugins/password/drivers/modoboa.php | 29 + public_html/plugins/password/password.php | 2 public_html/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php | 5 tests/Framework/Mime.php | 15 + tests/Framework/Utils.php | 22 + tests/Framework/Washtml.php | 49 +++ tests/Rcmail/Sendmail.php | 20 + tests/src/logo.jpg |binary 35 files changed, 462 insertions(+), 152 deletions(-) diff -Nru roundcube-1.6.19+dfsg/CHANGELOG.md roundcube-1.6.20+dfsg/CHANGELOG.md --- roundcube-1.6.19+dfsg/CHANGELOG.md 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/CHANGELOG.md 2026-10-19 08:49:43.000000000 +0200 @@ -2,6 +2,29 @@ ## Unreleased +- Security: Fix IMAP command injection via CRLF in `_uid` parameter of mail/headers and mail/viewsource action +- Security: Fix bug where an alias login wasn't qualified by IMAP host +- Security: Add a size limit to the CSS proxy content processing +- Security: Fix SSRF filter (is_local_url()) bypass via NAT64 64:ff9b::/96 +- Security: Fix remote content blocking bypass via SVG SMIL `by` attribute +- Security: Fix remote content blocking bypass via CSS escapes in FuncIRI attributes (again) +- Security: Fix passing potentally unsafe attachment content into ImageMagick on image resize +- Security: Fix mail header injection via the From display-name in the Bounce (resend) action +- Security: Fix mail header injection via References/In-Reply-To/Message-ID/From parameters +- Security: Fix object injection primitive in rcube_session::unserialize() via "!"-prefixed session keys +- Security: Fix ImageMagick resource limits bypass by GD fallback during thumbnail generation +- Security: Fix CSS scoping bypass in HTML messages using at-rules +- Security: Fix bug where deprecated action aliases bypassed `disabled_actions` setting +- Security: Fix IMAP command injection via CRLF in `_flag` parameter of mail/mark action +- Security: Fix CSRF validation bypass in the mark-as-junk action +- Security: Fix XSS issue in handling of password driver error responses +- Security: Fix mail header injection via subject of a message forwarded as attachment +- Security: Fix mail header injection via media type in an inline image data URI +- Security: Fix bug where Password Modoboa driver could change password for another account +- Security: Fix unsafe use of a login name in SquirrelMail import file path + +## Release 1.6.19 + - Fix PHP Warning: Undefined variable $tmp_command in .../plugins/markasjunk/drivers/cmd_learn.php (#10294) - Security: Fix CSS declaration smuggling via un-encoded ampersand emission - Security: Fix CSS property injection via body `background` attribute diff -Nru roundcube-1.6.19+dfsg/debian/changelog roundcube-1.6.20+dfsg/debian/changelog --- roundcube-1.6.19+dfsg/debian/changelog 2026-09-06 13:22:41.000000000 +0200 +++ roundcube-1.6.20+dfsg/debian/changelog 2026-10-20 07:00:29.000000000 +0200 @@ -1,3 +1,41 @@ +roundcube (1.6.20+dfsg-0+deb13u1) trixie-security; urgency=high + + * New upstream security and bugfix release (closes: #1151279). + + Fix CVE-2026-113386: IMAP command injection via CRLF in `_uid` parameter + of mail/headers and mail/viewsource action. + + Fix bug where an alias login wasn't qualified by IMAP host. + + Add a size limit to the CSS proxy content processing. + + Fix SSRF filter (`is_local_url()`) bypass via NAT64 64:ff9b::/96. + + Fix remote content blocking bypass via SVG SMIL `by` attribute. + + Fix remote content blocking bypass via CSS escapes in FuncIRI attributes + (again). + + Fix passing potentially unsafe attachment content into ImageMagick on + image resize. + + Fix mail header injection via the From display-name in the Bounce (resend) + action. + + Fix mail header injection via References/In-Reply-To/Message-ID/From + parameters. + + Fix object injection primitive in rcube_session::unserialize() via + “!”-prefixed session keys. + + Fix ImageMagick resource limits bypass by GD fallback during thumbnail + generation. + + Fix CSS scoping bypass in HTML messages using at-rules. + + Fix bug where deprecated action aliases bypassed `disabled_actions` + setting. + + Fix IMAP command injection via CRLF in `_flag` parameter of mail/mark + action. + + Fix CSRF validation bypass in the mark-as-junk action. + + Fix XSS issue in handling of password driver error responses. + + Fix mail header injection via subject of a message forwarded as + attachment. + + Fix mail header injection via media type in an inline image data URI. + + Fix bug where Password Modoboa driver could change password for another + account. + + Fix unsafe use of a login name in SquirrelMail import file path. + * Refresh d/patches. + + -- Guilhem Moulin <[email protected]> Tue, 20 Oct 2026 07:00:29 +0200 + roundcube (1.6.19+dfsg-0+deb13u1) trixie-security; urgency=high * New upstream security and bugfix release (closes: #1146838). diff -Nru roundcube-1.6.19+dfsg/debian/patches/Avoid-dependency-on-new-package-mlocati-ip-lib.patch roundcube-1.6.20+dfsg/debian/patches/Avoid-dependency-on-new-package-mlocati-ip-lib.patch --- roundcube-1.6.19+dfsg/debian/patches/Avoid-dependency-on-new-package-mlocati-ip-lib.patch 2026-09-06 13:22:41.000000000 +0200 +++ roundcube-1.6.20+dfsg/debian/patches/Avoid-dependency-on-new-package-mlocati-ip-lib.patch 2026-10-20 07:00:29.000000000 +0200 @@ -11,9 +11,9 @@ Forwarded: not-needed --- composer.json-dist | 3 +- - program/lib/Roundcube/rcube_utils.php | 163 +++++++++++++++++++++++++++++----- - tests/Framework/Utils.php | 85 ++++++++++++++++++ - 3 files changed, 225 insertions(+), 26 deletions(-) + program/lib/Roundcube/rcube_utils.php | 169 ++++++++++++++++++++++++++++------ + tests/Framework/Utils.php | 85 +++++++++++++++++ + 3 files changed, 228 insertions(+), 29 deletions(-) diff --git a/composer.json-dist b/composer.json-dist index 1807004..ca3de26 100644 @@ -30,7 +30,7 @@ "require-dev": { "phpunit/phpunit": "^9" diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php -index b10103e..2c1aff8 100644 +index 270a14f..94f0fc4 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -1,8 +1,5 @@ @@ -171,7 +171,7 @@ $host = trim($host, '[].'); if (preg_match('/([0-9a-f.-]+)\.(nip|sslip)\.io$/i', $host, $matches)) { -@@ -452,26 +554,39 @@ class rcube_utils +@@ -452,29 +554,42 @@ class rcube_utils } } @@ -192,9 +192,12 @@ - '169.254.0.0/16', // link-local / cloud metadata - '100.64.0.0/10', // RFC6598: Shared Address Space (carrier-grade NAT) - '::1/128', +- '::/128', - 'fc00::/7', - 'fe80::/10', // IPv6 link-local - '::ffff:0:0/96', // RFC5156 +- '64:ff9b::/96', // RFC 6052 NAT64 (maps IPv4 incl. private ranges into IPv6) +- '64:ff9b:1::/48' // (RFC 8215 local-use NAT64) + if (is_string($address = \rcube_utils::inet_pton2($host))) { + $nets = [ + ['0.0.0.0', '0.0.0.0'], @@ -213,8 +216,11 @@ + } + } + $nets[] = ['::1', '::1']; ++ $nets[] = ['::', '::']; + $nets[] = ['fc00::', 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff']; + $nets[] = ['fe80::', 'febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff']; // IPv6 link-local ++ $nets[] = ['64:ff9b::', '64:ff9b::ffff:ffff']; // RFC 6052 NAT64 (maps IPv4 incl. private ranges into IPv6) ++ $nets[] = ['64:ff9b:1::', '64:ff9b:1:ffff:ffff:ffff:ffff:ffff']; // (RFC 8215 local-use NAT64) + } - foreach ($nets as $net) { @@ -228,10 +234,10 @@ } } diff --git a/tests/Framework/Utils.php b/tests/Framework/Utils.php -index 98f53d0..757c1c4 100644 +index 4325b0b..5769772 100644 --- a/tests/Framework/Utils.php +++ b/tests/Framework/Utils.php -@@ -571,6 +571,86 @@ class Framework_Utils extends TestCase +@@ -588,6 +588,86 @@ class Framework_Utils extends TestCase } } @@ -318,7 +324,7 @@ /** * Test is_local_url() * -@@ -590,12 +670,17 @@ class Framework_Utils extends TestCase +@@ -607,12 +687,17 @@ class Framework_Utils extends TestCase return [ // Local hosts ['https://127.0.0.1', true], diff -Nru roundcube-1.6.19+dfsg/debian/patches/Fix-FTBFS-with-phpunit-11.patch roundcube-1.6.20+dfsg/debian/patches/Fix-FTBFS-with-phpunit-11.patch --- roundcube-1.6.19+dfsg/debian/patches/Fix-FTBFS-with-phpunit-11.patch 2026-09-06 13:22:41.000000000 +0200 +++ roundcube-1.6.20+dfsg/debian/patches/Fix-FTBFS-with-phpunit-11.patch 2026-10-20 07:00:29.000000000 +0200 @@ -8765,7 +8765,7 @@ } } diff --git a/tests/Framework/Mime.php b/tests/Framework/Mime.php -index abe9939..81d42f0 100644 +index b4266fd..602ffa9 100644 --- a/tests/Framework/Mime.php +++ b/tests/Framework/Mime.php @@ -1,11 +1,13 @@ @@ -8803,7 +8803,7 @@ $this->assertEquals($results[$idx], $res, "Decode address groups (#$idx)"); } -@@ -174,7 +176,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -189,7 +191,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase ]; foreach ($test as $idx => $item) { @@ -8812,7 +8812,7 @@ $res = quoted_printable_encode($res); $this->assertEquals($item['out'], $res, "Header decoding for: " . $idx); -@@ -186,7 +188,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -201,7 +203,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase */ function test_parse_headers() { @@ -8821,7 +8821,7 @@ $headers = "Subject: Test\r\n" -@@ -197,7 +199,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -212,7 +214,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase 'to' => '[email protected] [email protected]' ]; @@ -8830,7 +8830,7 @@ } /** -@@ -208,7 +210,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -223,7 +225,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase $raw = file_get_contents(TESTS_DIR . 'src/format-flowed-unfolded.txt'); $flowed = file_get_contents(TESTS_DIR . 'src/format-flowed.txt'); @@ -8839,7 +8839,7 @@ } /** -@@ -219,7 +221,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -234,7 +236,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase $flowed = file_get_contents(TESTS_DIR . 'src/format-flowed.txt'); $unfolded = file_get_contents(TESTS_DIR . 'src/format-flowed-unfolded.txt'); @@ -8848,7 +8848,7 @@ } /** -@@ -234,7 +236,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -249,7 +251,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase ."> \r\n" ."Sed ut perspiciatis unde omnis iste natus error sit voluptatem"; @@ -8857,7 +8857,7 @@ } /** -@@ -248,7 +250,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -263,7 +265,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase ."しているのを見ました。"; $unfolded = "そしてジョバンニはすぐうしろの天気輪の柱がいつかぼんやりした三角標の形になって、しばらく蛍のように、ぺかぺか消えたりともったりしているのを見ました。"; @@ -8866,7 +8866,7 @@ } /** -@@ -306,8 +308,8 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -321,8 +323,8 @@ class Framework_Mime extends PHPUnit\Framework\TestCase "this-is-just-some-blabla-to-make-this-more-than-seventy-five-characters-in-a-row\n-- this line should\nbe wrapped", ], [ @@ -8877,7 +8877,7 @@ ], ]; -@@ -322,9 +324,9 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -337,9 +339,9 @@ class Framework_Mime extends PHPUnit\Framework\TestCase function test_parse_message() { $file = file_get_contents(__DIR__ . '/../src/html.msg'); @@ -8889,7 +8889,7 @@ $this->assertSame('multipart/alternative', $result->mimetype); $this->assertSame('1.0', $result->headers['mime-version']); $this->assertSame('=_68eeaf4ab95b5312965e45c33362338e', $result->ctype_parameters['boundary']); -@@ -355,9 +357,9 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -370,9 +372,9 @@ class Framework_Mime extends PHPUnit\Framework\TestCase function test_file_content_type() { $file = INSTALL_PATH . 'program/resources/blocked.gif'; @@ -8901,7 +8901,7 @@ } /** -@@ -365,10 +367,10 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -380,10 +382,10 @@ class Framework_Mime extends PHPUnit\Framework\TestCase */ function test_get_mime_extensions() { @@ -8916,7 +8916,7 @@ } /** -@@ -377,7 +379,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -392,7 +394,7 @@ class Framework_Mime extends PHPUnit\Framework\TestCase function test_image_content_type() { $file = file_get_contents(INSTALL_PATH . 'program/resources/blocked.gif'); @@ -8925,7 +8925,7 @@ } /** -@@ -385,9 +387,9 @@ class Framework_Mime extends PHPUnit\Framework\TestCase +@@ -400,9 +402,9 @@ class Framework_Mime extends PHPUnit\Framework\TestCase */ function test_fix_mimetype() { @@ -10052,7 +10052,7 @@ $idents = $user->list_identities(); diff --git a/tests/Framework/Utils.php b/tests/Framework/Utils.php -index 26cf452..98f53d0 100644 +index 08d0880..4325b0b 100644 --- a/tests/Framework/Utils.php +++ b/tests/Framework/Utils.php @@ -1,11 +1,15 @@ @@ -10283,7 +10283,7 @@ $this->assertSame("#rcmbody { color: red; }", $mod); $style = 'body { background:url(alert('URL!')); }'; -@@ -343,7 +352,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -360,7 +369,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase :root * { color: red; } :root > * { top: 0; } '; @@ -10292,7 +10292,7 @@ $this->assertStringContainsString('#rc .testone', $mod); $this->assertStringContainsString('#rc .testthree.testfour', $mod); -@@ -361,24 +370,24 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -378,24 +387,24 @@ class Framework_Utils extends PHPUnit\Framework\TestCase function test_xss_entity_decode() { @@ -10322,7 +10322,7 @@ { return [ [ -@@ -453,9 +462,10 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -470,9 +479,10 @@ class Framework_Utils extends PHPUnit\Framework\TestCase * * @dataProvider data_parse_css_block */ @@ -10334,7 +10334,7 @@ } /** -@@ -470,7 +480,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -487,7 +497,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($data as $text => $res) { @@ -10343,7 +10343,7 @@ $this->assertSame($res, $result); } } -@@ -483,7 +493,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -500,7 +510,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase $data = ['', 'a,b,c', 'a', ',', ',a']; foreach ($data as $text) { @@ -10352,7 +10352,7 @@ $this->assertSame(explode(',', $text), $result); } } -@@ -498,7 +508,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -515,7 +525,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($input as $idx => $value) { @@ -10361,7 +10361,7 @@ } $input = [ -@@ -506,7 +516,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -523,7 +533,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($input as $idx => $value) { @@ -10370,7 +10370,7 @@ } } -@@ -516,13 +526,13 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -533,13 +543,13 @@ class Framework_Utils extends PHPUnit\Framework\TestCase function test_get_input_string() { $_GET = []; @@ -10387,7 +10387,7 @@ } /** -@@ -530,18 +540,18 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -547,18 +557,18 @@ class Framework_Utils extends PHPUnit\Framework\TestCase */ function test_is_simple_string() { @@ -10418,7 +10418,7 @@ } /** -@@ -556,7 +566,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -573,7 +583,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $v) { @@ -10427,7 +10427,7 @@ $this->assertSame($v[2], $result); } } -@@ -646,7 +656,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -668,7 +678,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $datetime => $ts) { @@ -10436,7 +10436,7 @@ $this->assertSame($ts, $result, "Error parsing date: $datetime"); } } -@@ -673,7 +683,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -695,7 +705,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $datetime => $ts) { @@ -10445,7 +10445,7 @@ $this->assertSame($ts, $result ? $result->format('Y-m-d') : false, "Error parsing date: $datetime"); } -@@ -683,7 +693,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -705,7 +715,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $datetime => $ts) { @@ -10454,7 +10454,7 @@ $this->assertSame($ts, $result ? $result->format('Y-m-d H:i:s') : false, "Error parsing date: $datetime"); } -@@ -692,7 +702,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -714,7 +724,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $datetime => $ts) { @@ -10463,7 +10463,7 @@ $this->assertSame($ts, $result ? $result->format('Y-m-d H:i:s O') : false, "Error parsing date: $datetime"); } } -@@ -702,17 +712,17 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -724,17 +734,17 @@ class Framework_Utils extends PHPUnit\Framework\TestCase */ function test_anytodatetime_timezone() { @@ -10484,7 +10484,7 @@ if ($result) $result->setTimezone($tz); // move to target timezone for comparison $this->assertSame($ts, $result ? $result->format('Y-m-d H:i') : false, "Error parsing date: $datetime"); } -@@ -731,7 +741,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -753,7 +763,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $data) { @@ -10493,7 +10493,7 @@ $this->assertSame($data[2], $result, "Error formatting date: " . $data[0]); } } -@@ -750,7 +760,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -772,7 +782,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $input => $output) { @@ -10502,7 +10502,7 @@ $this->assertSame($output, $result); } } -@@ -775,7 +785,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -797,7 +807,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $input => $output) { @@ -10511,7 +10511,7 @@ $this->assertSame($output, $result, "Error normalizing '$input'"); } } -@@ -798,7 +808,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -820,7 +830,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase ]; foreach ($test as $idx => $params) { @@ -10520,7 +10520,7 @@ $this->assertSame($params[2], $result, "words_match() at index $idx"); } } -@@ -824,7 +834,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -846,7 +856,7 @@ class Framework_Utils extends PHPUnit\Framework\TestCase } foreach ($test as $input => $output) { @@ -10529,7 +10529,7 @@ $this->assertSame($output, $result); } } -@@ -834,17 +844,17 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -856,17 +866,17 @@ class Framework_Utils extends PHPUnit\Framework\TestCase */ function test_random_bytes() { @@ -10553,7 +10553,7 @@ { /* -@@ -881,9 +891,10 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -903,9 +913,10 @@ class Framework_Utils extends PHPUnit\Framework\TestCase * @param string $encoded Encoded email address * @dataProvider data_idn_convert */ @@ -10565,7 +10565,7 @@ } /** -@@ -893,9 +904,10 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -915,9 +926,10 @@ class Framework_Utils extends PHPUnit\Framework\TestCase * @param string $encoded Encoded email address * @dataProvider data_idn_convert */ @@ -10577,7 +10577,7 @@ } /** -@@ -903,14 +915,14 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -925,14 +937,14 @@ class Framework_Utils extends PHPUnit\Framework\TestCase */ function test_idn_to_ascii_special() { @@ -10595,7 +10595,7 @@ { return [ ['%z', 'hostname', 'hostname'], -@@ -925,15 +937,16 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -947,15 +959,16 @@ class Framework_Utils extends PHPUnit\Framework\TestCase * * @dataProvider data_parse_host */ @@ -10614,7 +10614,7 @@ { return [ [['hostname', null, null], ['hostname', null, null]], -@@ -956,15 +969,16 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -978,15 +991,16 @@ class Framework_Utils extends PHPUnit\Framework\TestCase * * @dataProvider data_parse_host_uri */ @@ -10633,7 +10633,7 @@ return [ ['both', 'Fwd: Re: Test subject both', 'Test subject both'], ['both', 'Re: Fwd: Test subject both', 'Test subject both'], -@@ -982,8 +996,9 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -1004,8 +1018,9 @@ class Framework_Utils extends PHPUnit\Framework\TestCase * * @dataProvider data_remove_subject_prefix */ @@ -10644,7 +10644,7 @@ } /** -@@ -991,13 +1006,13 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -1013,13 +1028,13 @@ class Framework_Utils extends PHPUnit\Framework\TestCase */ function test_server_name() { @@ -10661,7 +10661,7 @@ } /** -@@ -1007,31 +1022,31 @@ class Framework_Utils extends PHPUnit\Framework\TestCase +@@ -1029,31 +1044,31 @@ class Framework_Utils extends PHPUnit\Framework\TestCase { $_SERVER['test'] = 'test.com'; @@ -10860,7 +10860,7 @@ $this->assertSame($result, "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:\r\nN:;;;;\r\nEND:VCARD"); diff --git a/tests/Framework/Washtml.php b/tests/Framework/Washtml.php -index 03ab2db..a61f7da 100644 +index 794a077..85cbe9b 100644 --- a/tests/Framework/Washtml.php +++ b/tests/Framework/Washtml.php @@ -1,11 +1,14 @@ @@ -11061,7 +11061,7 @@ { $svg1 = "<svg id='x' width='100' height='100'><a xlink:href='javascript:alert(1)'><rect x='0' y='0' width='100' height='100' /></a></svg>"; -@@ -573,9 +576,10 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -622,9 +625,10 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase * * @dataProvider data_wash_svg_tests */ @@ -11073,7 +11073,7 @@ $washed = $washer->wash($input); $this->assertSame($expected, $this->cleanupResult($washed), "SVG content"); -@@ -584,7 +588,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -633,7 +637,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase /** * Test cases for various XSS issues */ @@ -11082,7 +11082,7 @@ { return [ [ -@@ -660,9 +664,10 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -709,9 +713,10 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase * * @dataProvider data_wash_xss_tests */ @@ -11094,7 +11094,7 @@ $washed = $washer->wash($input); $this->assertSame($expected, $this->cleanupResult($washed), "XSS issues"); -@@ -676,7 +681,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -725,7 +730,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase $html = "<img style='position:fixed' /><img style=\"position:/**/ fixed; top:10px\" />"; $exp = "<img style=\"position: absolute\" /><img style=\"position: absolute; top: 10px\" />"; @@ -11103,7 +11103,7 @@ $washed = $washer->wash($html); $this->assertTrue(strpos($washed, $exp) !== false, "Position:fixed (#5264)"); -@@ -720,7 +725,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -769,7 +774,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase <annotation encoding="TeX">I_D = \frac{1}{2} k_n \frac{W}{L} (V_{GS}-V_t)^2</annotation> </semantics></math>'; @@ -11112,7 +11112,7 @@ $washed = $washer->wash($mathml); // remove whitespace between tags -@@ -737,7 +742,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -786,7 +791,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase { $html = "<input type=\"image\" src=\"http://TRACKING_URL/\">"; @@ -11121,7 +11121,7 @@ $washed = $washer->wash($html); $this->assertTrue($washer->extlinks); -@@ -745,7 +750,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -794,7 +799,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase $html = "<video src=\"http://TRACKING_URL/\">"; @@ -11130,7 +11130,7 @@ $washed = $washer->wash($html); $this->assertTrue($washer->extlinks); -@@ -773,14 +778,14 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -822,14 +827,14 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase ]; foreach ($html as $item) { @@ -11147,7 +11147,7 @@ $washed = $washer->wash($item[0]); $this->assertFalse($washer->extlinks); -@@ -791,7 +796,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -840,7 +845,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase { $html = '<textarea><p style="x:</textarea><img src=x onerror=alert(1)>">'; @@ -11156,7 +11156,7 @@ $washed = $washer->wash($html); $this->assertStringNotContainsString('onerror=alert(1)>', $washed); -@@ -803,7 +808,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -852,7 +857,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase */ function test_css_prefix() { @@ -11165,7 +11165,7 @@ $html = '<p id="my-id">' . '<label for="my-other-id" class="my-class1 my-class2">test</label>' -@@ -831,14 +836,14 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -880,14 +885,14 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase { $html = '<p><?xml:namespace prefix = "xsl" /></p>'; @@ -11182,7 +11182,7 @@ $washed = $this->cleanupResult($washer->wash($html)); $this->assertSame($washed, 'HTML'); -@@ -849,7 +854,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -898,7 +903,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase */ function test_missing_tags() { @@ -11191,7 +11191,7 @@ $html = '<head></head>First line<br />Second line'; $washed = $washer->wash($html); -@@ -891,7 +896,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -940,7 +945,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase { $html = '<p><![CDATA[<script>alert(document.cookie)</script>]]></p>'; @@ -11200,7 +11200,7 @@ $washed = $washer->wash($html); $this->assertTrue(strpos($washed, '<script>') === false, "CDATA content"); -@@ -903,7 +908,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -952,7 +957,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase function test_resolve_base() { $html = file_get_contents(TESTS_DIR . 'src/htmlbase.txt'); @@ -11209,7 +11209,7 @@ $this->assertMatchesRegularExpression('|src="http://alec\.pl/dir/img1\.gif"|', $html, "URI base resolving [1]"); $this->assertMatchesRegularExpression('|src="http://alec\.pl/dir/img2\.gif"|', $html, "URI base resolving [2]"); -@@ -949,7 +954,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase +@@ -998,7 +1003,7 @@ class Framework_Washtml extends PHPUnit\Framework\TestCase <tr><td></td></tr> </table>'; @@ -12050,7 +12050,7 @@ $result = $mail->headers(); diff --git a/tests/Rcmail/Sendmail.php b/tests/Rcmail/Sendmail.php -index 9ccb87e..57183e8 100644 +index a29fcf2..7b61e95 100644 --- a/tests/Rcmail/Sendmail.php +++ b/tests/Rcmail/Sendmail.php @@ -1,11 +1,14 @@ @@ -12093,7 +12093,7 @@ $result = $sendmail->get_identity($identity['identity_id']); -@@ -97,7 +100,7 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -115,7 +118,7 @@ class Rcmail_RcmailSendmail extends ActionTestCase /** * Data for test_convert() */ @@ -12102,7 +12102,7 @@ { return [ [ -@@ -156,9 +159,10 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -174,9 +177,10 @@ class Rcmail_RcmailSendmail extends ActionTestCase /** * @dataProvider data_email_input_format */ @@ -12114,7 +12114,7 @@ $sendmail->options['charset'] = $charset; $this->assertEquals($output, $sendmail->email_input_format($input)); -@@ -178,24 +182,24 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -196,24 +200,24 @@ class Rcmail_RcmailSendmail extends ActionTestCase function test_draftinfo_encode_and_decode() { $input = ['test' => 'test']; @@ -12147,7 +12147,7 @@ } /** -@@ -203,14 +207,14 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -221,14 +225,14 @@ class Rcmail_RcmailSendmail extends ActionTestCase */ function test_headers_output() { @@ -12165,7 +12165,7 @@ $sendmail->options['charset'] = RCUBE_CHARSET; $sendmail->options['message'] = $message; -@@ -229,10 +233,10 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -247,10 +251,10 @@ class Rcmail_RcmailSendmail extends ActionTestCase */ function test_reply_subject() { @@ -12180,7 +12180,7 @@ } /** -@@ -240,9 +244,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -258,9 +262,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase */ function test_compose_subject() { @@ -12192,7 +12192,7 @@ $_POST = ['_subject' => 'test']; -@@ -256,9 +260,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -274,9 +278,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase */ function test_mdn_checkbox() { @@ -12204,7 +12204,7 @@ $result = $sendmail->mdn_checkbox([]); -@@ -270,9 +274,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -288,9 +292,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase */ function test_dsn_checkbox() { @@ -12216,7 +12216,7 @@ $result = $sendmail->dsn_checkbox([]); -@@ -284,9 +288,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -302,9 +306,9 @@ class Rcmail_RcmailSendmail extends ActionTestCase */ function test_priority_selector() { @@ -12228,7 +12228,7 @@ $result = $sendmail->priority_selector([]); -@@ -306,14 +310,14 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -324,14 +328,14 @@ class Rcmail_RcmailSendmail extends ActionTestCase */ function test_identity_select() { @@ -12246,7 +12246,7 @@ $this->assertSame(null, $result); $identities = [ -@@ -349,26 +353,26 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -367,26 +371,26 @@ class Rcmail_RcmailSendmail extends ActionTestCase $message->headers->to = '[email protected]'; $message->headers->from = '[email protected]'; @@ -12277,7 +12277,7 @@ $this->assertSame($identities[2], $result); } -@@ -395,15 +399,15 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -413,15 +417,15 @@ class Rcmail_RcmailSendmail extends ActionTestCase ], ]; @@ -12297,7 +12297,7 @@ $this->assertSame($identities[1], $res); } -@@ -436,23 +440,23 @@ class Rcmail_RcmailSendmail extends ActionTestCase +@@ -454,23 +458,23 @@ class Rcmail_RcmailSendmail extends ActionTestCase ], ]; diff -Nru roundcube-1.6.19+dfsg/index.php roundcube-1.6.20+dfsg/index.php --- roundcube-1.6.19+dfsg/index.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/index.php 2026-10-19 08:49:43.000000000 +0200 @@ -264,15 +264,5 @@ $RCMAIL->output->send($plugin['task']); } -else { - // CSRF prevention - $RCMAIL->request_security_check(); - - // check access to disabled actions - $disabled_actions = (array) $RCMAIL->config->get('disabled_actions'); - if (in_array($RCMAIL->task . '.' . ($RCMAIL->action ?: 'index'), $disabled_actions)) { - rcube::raise_error(['code' => 404, 'message' => "Action disabled"], true, true); - } -} $RCMAIL->action_handler(); diff -Nru roundcube-1.6.19+dfsg/plugins/markasjunk/markasjunk.php roundcube-1.6.20+dfsg/plugins/markasjunk/markasjunk.php --- roundcube-1.6.19+dfsg/plugins/markasjunk/markasjunk.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/plugins/markasjunk/markasjunk.php 2026-10-19 08:49:43.000000000 +0200 @@ -140,7 +140,7 @@ $is_spam = $this->rcube->action == 'plugin.markasjunk.junk'; $uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox_name = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST); - $messageset = rcmail::get_uids($uids, $mbox_name, $multifolder); + $messageset = rcmail::get_uids($uids, $mbox_name, $multifolder, rcube_utils::INPUT_POST); $dest_mbox = $is_spam ? $this->spam_mbox : $this->ham_mbox; // special case when select all is used, uid is '*', and not in multi folder mode and we are using a driver diff -Nru roundcube-1.6.19+dfsg/plugins/password/drivers/directadmin.php roundcube-1.6.20+dfsg/plugins/password/drivers/directadmin.php --- roundcube-1.6.19+dfsg/plugins/password/drivers/directadmin.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/plugins/password/drivers/directadmin.php 2026-10-19 08:49:43.000000000 +0200 @@ -60,7 +60,7 @@ } if (isset($response['error']) && $response['error'] == 1) { - return ['code' => PASSWORD_ERROR, 'message' => strip_tags($response['text'])]; + return ['code' => PASSWORD_ERROR, 'message' => $response['text']]; } return PASSWORD_SUCCESS; diff -Nru roundcube-1.6.19+dfsg/plugins/password/drivers/modoboa.php roundcube-1.6.20+dfsg/plugins/password/drivers/modoboa.php --- roundcube-1.6.19+dfsg/plugins/password/drivers/modoboa.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/plugins/password/drivers/modoboa.php 2026-10-19 08:49:43.000000000 +0200 @@ -82,20 +82,35 @@ } // Get user ID (pk) - $userid = $decoded[0]->pk; + $userid = null; + $user = null; + foreach ($decoded as $account) { + if (isset($account->username) && $account->username === $username) { + $userid = $account->pk; + $user = [ + 'username' => $account->username, + 'mailbox' => $account->mailbox, + 'role' => $account->role, + ]; + break; + } + } + + if ($user === null) { + return PASSWORD_CONNECT_ERROR; + } // Encode json with new password - $ret['username'] = $decoded[0]->username; - $ret['mailbox'] = $decoded[0]->mailbox; - $ret['role'] = $decoded[0]->role; - $ret['password'] = $passwd; // new password - $encoded = json_encode($ret); + $user['password'] = $passwd; + $encoded = json_encode($user); + + $url = "https://{$host}/api/v1/accounts/{$userid}/"; // Call HTTP API Modoboa $curl = curl_init(); curl_setopt_array($curl, [ - CURLOPT_URL => "https://" . $host . "/api/v1/accounts/" . $userid . "/", + CURLOPT_URL => "https://{$host}/api/v1/accounts/{$userid}/", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, diff -Nru roundcube-1.6.19+dfsg/plugins/password/password.php roundcube-1.6.20+dfsg/plugins/password/password.php --- roundcube-1.6.19+dfsg/plugins/password/password.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/plugins/password/password.php 2026-10-19 08:49:43.000000000 +0200 @@ -425,7 +425,7 @@ } if ($message) { - $reason .= ' ' . $message; + $reason .= ' ' . rcube::Q($message, 'strict', false); } return $reason; diff -Nru roundcube-1.6.19+dfsg/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php roundcube-1.6.20+dfsg/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php --- roundcube-1.6.19+dfsg/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php 2026-10-19 08:49:43.000000000 +0200 @@ -170,7 +170,10 @@ $rcmail = rcmail::get_instance(); /**** File based backend ****/ - if ($rcmail->config->get('squirrelmail_driver') == 'file' && ($srcdir = $rcmail->config->get('squirrelmail_data_dir'))) { + if ($rcmail->config->get('squirrelmail_driver') == 'file' + && ($srcdir = $rcmail->config->get('squirrelmail_data_dir')) + && strpos($uname, '/') === false // security check + ) { if (($hash_level = $rcmail->config->get('squirrelmail_data_dir_hash_level')) > 0) { $srcdir = slashify($srcdir).chunk_split(substr(base_convert(crc32($uname), 10, 16), 0, $hash_level), 1, '/'); } diff -Nru roundcube-1.6.19+dfsg/program/actions/contacts/upload_photo.php roundcube-1.6.20+dfsg/program/actions/contacts/upload_photo.php --- roundcube-1.6.19+dfsg/program/actions/contacts/upload_photo.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/actions/contacts/upload_photo.php 2026-10-19 08:49:43.000000000 +0200 @@ -25,7 +25,7 @@ * * @var array */ - public static $IMAGE_TYPES = ['jpeg','jpg','jp2','tiff','tif','bmp','eps','gif','png','png8','png24','png32','svg','ico']; + public static $IMAGE_TYPES = ['jpeg', 'jpg', 'jp2', 'tiff', 'tif', 'bmp', 'gif', 'png', 'png8', 'png24', 'png32', 'svg', 'ico']; /** * Request handler. diff -Nru roundcube-1.6.19+dfsg/program/actions/mail/compose.php roundcube-1.6.20+dfsg/program/actions/mail/compose.php --- roundcube-1.6.19+dfsg/program/actions/mail/compose.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/actions/mail/compose.php 2026-10-19 08:49:43.000000000 +0200 @@ -1214,7 +1214,11 @@ } // generate (unique) attachment name - $name = strlen($message->subject) ? mb_substr($message->subject, 0, 64) : 'message_rfc822'; + $name = 'message_rfc822'; + if (strlen($message->subject)) { + // Note: Replace any whitespace characters (most importantly \r and \n) + $name = mb_substr(trim(preg_replace('/\s+/', ' ', $message->subject)), 0, 64); + } if (!empty($names[$name])) { $names[$name]++; $name .= '_' . $names[$name]; diff -Nru roundcube-1.6.19+dfsg/program/actions/mail/headers.php roundcube-1.6.20+dfsg/program/actions/mail/headers.php --- roundcube-1.6.19+dfsg/program/actions/mail/headers.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/actions/mail/headers.php 2026-10-19 08:49:43.000000000 +0200 @@ -32,7 +32,8 @@ $uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GP); $inline = $rcmail->output instanceof rcmail_output_html; - if (!$uid) { + if (!$uid || preg_match('/[\r\n]/', $uid)) { + rcube::raise_error("Message UID {$uid} not found", true, true); exit; } diff -Nru roundcube-1.6.19+dfsg/program/actions/mail/viewsource.php roundcube-1.6.20+dfsg/program/actions/mail/viewsource.php --- roundcube-1.6.19+dfsg/program/actions/mail/viewsource.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/actions/mail/viewsource.php 2026-10-19 08:49:43.000000000 +0200 @@ -35,7 +35,8 @@ ob_end_clean(); // similar code as in program/steps/mail/get.inc - if ($uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET)) { + $uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET); + if ($uid && !preg_match('/[\r\n]/', $uid)) { if ($pos = strpos($uid, '.')) { $message = new rcube_message($uid); $headers = $message->headers; diff -Nru roundcube-1.6.19+dfsg/program/actions/utils/modcss.php roundcube-1.6.20+dfsg/program/actions/utils/modcss.php --- roundcube-1.6.19+dfsg/program/actions/utils/modcss.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/actions/utils/modcss.php 2026-10-19 08:49:43.000000000 +0200 @@ -20,6 +20,8 @@ class rcmail_action_utils_modcss extends rcmail_action { + private const MAX_SIZE = 5 * 1024 * 1024; + /** * Request handler. * @@ -47,14 +49,25 @@ $ctype = null; try { - $client = rcube::get_instance()->get_http_client(['allow_redirects' => false]); + // Be strict, and limit the response size + $options = [ + 'allow_redirects' => false, + 'read_timeout' => 10, + 'stream' => true, + ]; + + $client = rcube::get_instance()->get_http_client($options); $response = $client->get($realurl); - if (!empty($response)) { - $ctype = $response->getHeader('Content-Type'); - $ctype = !empty($ctype) ? $ctype[0] : ''; - $source = $response->getBody(); + $ctype = $response->getHeader('Content-Type'); + $ctype = !empty($ctype) ? $ctype[0] : ''; + + $stream = $response->getBody(); + $source = ''; + while (!$stream->eof() && strlen($source) < self::MAX_SIZE) { + $source .= $stream->read(1024); } + $stream->close(); } catch (Exception $e) { rcube::raise_error($e, true, false); diff -Nru roundcube-1.6.19+dfsg/program/include/rcmail_attachment_handler.php roundcube-1.6.20+dfsg/program/include/rcmail_attachment_handler.php --- roundcube-1.6.19+dfsg/program/include/rcmail_attachment_handler.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/include/rcmail_attachment_handler.php 2026-10-19 08:49:43.000000000 +0200 @@ -97,7 +97,8 @@ $file_id = preg_replace('/^rcmfile/', '', $file_id); $compose = $_SESSION['compose_data_' . $compose_id] ?? null; - if ($compose && ($this->upload = $compose['attachments'][$file_id])) { + if ($compose && !empty($compose['attachments'][$file_id])) { + $this->upload = $compose['attachments'][$file_id]; $this->filename = $this->upload['name']; $this->mimetype = $this->upload['mimetype']; $this->size = $this->upload['size']; diff -Nru roundcube-1.6.19+dfsg/program/include/rcmail.php roundcube-1.6.20+dfsg/program/include/rcmail.php --- roundcube-1.6.19+dfsg/program/include/rcmail.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/include/rcmail.php 2026-10-19 08:49:43.000000000 +0200 @@ -224,6 +224,9 @@ */ public function action_handler() { + // CSRF prevention + $this->request_security_check(); + // we're ready, user is authenticated and the request is safe $plugin = $this->plugins->exec_hook('ready', ['task' => $this->task, 'action' => $this->action]); @@ -237,13 +240,27 @@ $this->output->send(); } - $task = $this->action == 'save-pref' ? 'utils' : $this->task; - $task = $task == 'addressbook' ? 'contacts' : $task; - $task_class = "rcmail_action_{$task}_index"; + $task = $this->action == 'save-pref' ? 'utils' : $this->task; + $task = $task == 'addressbook' ? 'contacts' : $task; + + // check access to disabled actions + $disabled_actions = (array) $this->config->get('disabled_actions'); + if (in_array($task . '.' . ($this->action ?: 'index'), $disabled_actions)) { + self::raise_error(['code' => 404, 'message' => 'Action disabled'], true, true); + } // execute the action index handler + $task_class = "rcmail_action_{$task}_index"; if (class_exists($task_class)) { - $task_handler = new $task_class; + $task_handler = new $task_class(); + + // check access to disabled actions (resolve an alias) + if ($this->action && !empty($task_handler::$aliases[$this->action])) { + if (in_array($task . '.' . $task_handler::$aliases[$this->action], $disabled_actions)) { + self::raise_error(['code' => 404, 'message' => 'Action disabled'], true, true); + } + } + $task_handler->run(); } diff -Nru roundcube-1.6.19+dfsg/program/include/rcmail_sendmail.php roundcube-1.6.20+dfsg/program/include/rcmail_sendmail.php --- roundcube-1.6.19+dfsg/program/include/rcmail_sendmail.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/include/rcmail_sendmail.php 2026-10-19 08:49:43.000000000 +0200 @@ -179,9 +179,6 @@ $message_id = $this->rcmail->gen_message_id($from); } - // Don't allow CRLF in subject (#8404) - $subject = trim(preg_replace('/[\r\n]+/', ' ', $subject)); - $this->options['dsn_enabled'] = $dsn_enabled; $this->options['from'] = $from; $this->options['mailto'] = $mailto; @@ -206,7 +203,7 @@ ]; if (!empty($identity_arr['organization'])) { - $headers['Organization'] = preg_replace('/[\r\n]+/', ' ', $identity_arr['organization']); + $headers['Organization'] = $identity_arr['organization']; } if ($mdn_enabled) { @@ -255,6 +252,13 @@ } } + // For security reasons do not allow new-line characters in headers + foreach ($headers as $idx => $value) { + if (is_string($value)) { + $headers[$idx] = trim(preg_replace('/[\r\n\t\s]+/', ' ', $value)); + } + } + return array_filter($headers); } @@ -695,7 +699,7 @@ $hash = md5($data) . '@' . $domain; $mime_type = $matches[2][$idx][0]; - if (empty($mime_type)) { + if (empty($mime_type) || !rcube_mime::is_mimetype_valid($mime_type)) { $mime_type = rcube_mime::image_content_type($data); } diff -Nru roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_image.php roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_image.php --- roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_image.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_image.php 2026-10-19 08:49:43.000000000 +0200 @@ -148,7 +148,8 @@ $result = ($this->image_file == $filename || copy($this->image_file, $filename)) ? '' : false; } else { - $valid_types = "bmp,eps,gif,jp2,jpg,png,svg,tif"; + // Note: We don't support types like svg or eps for security reasons + $valid_types = 'bmp,gif,jp2,jpg,png,tif'; if (in_array($type, explode(',', $valid_types))) { // Valid type? if ($scale >= 1) { @@ -364,7 +365,7 @@ $props = $this->props(); // do we have enough memory? (#1489937) - if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) { + if (!$this->mem_check($props)) { return false; } diff -Nru roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_imap_generic.php roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_imap_generic.php --- roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_imap_generic.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_imap_generic.php 2026-10-19 08:49:43.000000000 +0200 @@ -2353,6 +2353,10 @@ $flag = $this->flags[strtoupper($flag)]; } + if (preg_match('/[\x00-\x20\x7f(){"*%]/', $flag)) { + return false; + } + // if PERMANENTFLAGS is not specified all flags are allowed if (!empty($this->data['PERMANENTFLAGS']) && !in_array($flag, (array) $this->data['PERMANENTFLAGS']) diff -Nru roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_mime.php roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_mime.php --- roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_mime.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_mime.php 2026-10-19 08:49:43.000000000 +0200 @@ -117,7 +117,8 @@ $out[$j] = $address; } else { - $name = trim($val['name']); + // Remove any control characters for security reasons + $name = trim(preg_replace('/[\x00-\x1F]+/', ' ', $val['name'])); $string = ''; if ($name && $address && $name != $address) { diff -Nru roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_session.php roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_session.php --- roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_session.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_session.php 2026-10-19 08:49:43.000000000 +0200 @@ -520,11 +520,7 @@ break 2; if ($str[$p] == '!') { - $p++; - $has_value = false; - } - else { - $has_value = true; + throw new Exception('Unsupported value'); } $name = substr($str, $p, $q - $p); @@ -532,7 +528,8 @@ $serialized .= 's:' . strlen($name) . ':"' . $name . '";'; - if ($has_value) { + // @phpstan-ignore-next-line + if (true) { for (;;) { $p = $q; switch (strtolower($str[$q])) { diff -Nru roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_user.php roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_user.php --- roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_user.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_user.php 2026-10-19 08:49:43.000000000 +0200 @@ -600,8 +600,8 @@ $sql_result = $dbh->limitquery("SELECT u.*" . " FROM " . $dbh->table_name('users', true) . " u" . " JOIN " . $dbh->table_name('identities', true) . " i ON (i.`user_id` = u.`user_id`)" - . " WHERE `email` = ? AND `del` <> 1", - 0, 1, $user + . " WHERE u.`mail_host` = ? AND i.`email` = ? AND i.`del` <> 1", + 0, 1, $host, $user ); $sql_arr = $dbh->fetch_assoc($sql_result); diff -Nru roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_utils.php roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_utils.php --- roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_utils.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_utils.php 2026-10-19 08:49:43.000000000 +0200 @@ -464,9 +464,12 @@ '169.254.0.0/16', // link-local / cloud metadata '100.64.0.0/10', // RFC6598: Shared Address Space (carrier-grade NAT) '::1/128', + '::/128', 'fc00::/7', 'fe80::/10', // IPv6 link-local '::ffff:0:0/96', // RFC5156 + '64:ff9b::/96', // RFC 6052 NAT64 (maps IPv4 incl. private ranges into IPv6) + '64:ff9b:1::/48' // (RFC 8215 local-use NAT64) ]; foreach ($nets as $net) { @@ -568,9 +571,10 @@ if ($container_id || $prefix) { // Exclude rcube_string_replacer pattern matches, this is needed // for cases like @media { body { position: fixed; } } (#5811) - $excl = '(?!' . substr($replacements->pattern, 1, -1) . ')'; - $regexp = '/(^\s*|,\s*|\}\s*|\{\s*)(' . $excl . ':?[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im'; - $callback = function($matches) use ($container_id, $prefix) { + $excl = '(?!' . substr($replacements->pattern, 1, -1) . ')'; + $regexp = '/(^\s*|,\s*|\}\s*|\{\s*|@[^;{]*[;{]\s*)' + . '(' . $excl . ':?[a-z0-9\._#\*\[:][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im'; + $callback = function ($matches) use ($container_id, $prefix) { $replace = $matches[2]; if (stripos($replace, ':root') === 0) { diff -Nru roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_washtml.php roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_washtml.php --- roundcube-1.6.19+dfsg/program/lib/Roundcube/rcube_washtml.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/program/lib/Roundcube/rcube_washtml.php 2026-10-19 08:49:43.000000000 +0200 @@ -52,7 +52,13 @@ */ class rcube_washtml { - public const CSS_URL_REGEXP = '/(u|\\\0*75|\\\0*55)(r|\\\0*72|\\\0*52)(l|\\\0*6C|\\\0*4C)\(/i'; + /** + * Matches the CSS url() token in text that has been normalised by + * self::css_unescape(). Kept simple on purpose: every escaped spelling is + * resolved before this pattern is applied, so it never has to enumerate + * escape variants. + */ + public const CSS_URL_REGEXP = '/(u)(r)(l)\(/i'; /** * @var array Allowed HTML elements (default) @@ -310,7 +316,7 @@ $out = null; // in SVG to/from attribs may contain anything, including URIs - if ($key == 'to' || $key == 'from' || $key == 'values') { + if ($key == 'to' || $key == 'from' || $key == 'values' || $key == 'by') { $key = strtolower((string) $node->getAttribute('attributeName')); $key = trim(preg_replace('/^.*:/', '', $key)); if ($key && !isset($this->_html_attribs[$key])) { @@ -325,6 +331,9 @@ $out = $this->wash_link($value); } else if ($this->is_funciri_attribute($node->nodeName, $key)) { + // Resolve CSS escapes before looking for the url() token, so + // that no spelling of it has to be enumerated (#10325) + $value = self::css_unescape($value); if (preg_match(self::CSS_URL_REGEXP, $value, $m)) { $value = preg_replace(self::CSS_URL_REGEXP, 'url(', $value); if (preg_match('/^([a-z:]*url)\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)?/iu', $value, $match)) { @@ -560,8 +569,64 @@ } $rx = '/^(mask|cursor|fill|filter|stroke|clip-path|marker-start|marker-end|marker-mid)$/i'; - return self::attribute_value($node, 'attributeName', $rx) - && self::attribute_value($node, 'values', self::CSS_URL_REGEXP); + + if (!self::attribute_value($node, 'attributeName', $rx)) { + return false; + } + + // SMIL takes its value from any of these attributes, so all of them have + // to be inspected, not just 'values' + foreach (['values', 'to', 'from', 'by'] as $attrib) { + $value = $node->getAttribute($attrib); + if ($value !== '' && preg_match(self::CSS_URL_REGEXP, self::css_unescape($value))) { + return true; + } + } + + return false; + } + + /** + * Resolve CSS escape sequences so that a value can be matched against a + * plain pattern. + * + * CSS Syntax Level 3 (4.3.7) defines two escape forms: a backslash followed + * by one to six hex digits, optionally terminated by a single whitespace + * character, and a backslash followed by any other character, which stands + * for that character literally. Both are resolved here, so "u\72 l(", + * "\000075rl(" and "u\rl(" all reduce to "url(". + * + * Resolving first and matching afterwards means the caller does not have to + * enumerate spellings, which is what previously allowed new variants to slip + * through one at a time. + * + * @param string $value CSS text + * + * @return string Text with escape sequences resolved + */ + public static function css_unescape($value) + { + if (strpos((string) $value, '\\') === false) { + return (string) $value; + } + + return preg_replace_callback( + '/\\\(?:([0-9a-fA-F]{1,6})[ \t\n\r\f]?|([^\n\r\f]))/', + static function ($m) { + if ($m[1] !== '') { + $cp = hexdec($m[1]); + // Null, out-of-range and surrogate code points become U+FFFD + if ($cp === 0 || $cp > 0x10FFFF || ($cp >= 0xD800 && $cp <= 0xDFFF)) { + return "\u{FFFD}"; + } + + return mb_chr($cp, 'UTF-8'); + } + + return $m[2]; + }, + (string) $value + ); } /** diff -Nru roundcube-1.6.19+dfsg/public_html/plugins/markasjunk/markasjunk.php roundcube-1.6.20+dfsg/public_html/plugins/markasjunk/markasjunk.php --- roundcube-1.6.19+dfsg/public_html/plugins/markasjunk/markasjunk.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/public_html/plugins/markasjunk/markasjunk.php 2026-10-19 08:49:43.000000000 +0200 @@ -140,7 +140,7 @@ $is_spam = $this->rcube->action == 'plugin.markasjunk.junk'; $uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); $mbox_name = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST); - $messageset = rcmail::get_uids($uids, $mbox_name, $multifolder); + $messageset = rcmail::get_uids($uids, $mbox_name, $multifolder, rcube_utils::INPUT_POST); $dest_mbox = $is_spam ? $this->spam_mbox : $this->ham_mbox; // special case when select all is used, uid is '*', and not in multi folder mode and we are using a driver diff -Nru roundcube-1.6.19+dfsg/public_html/plugins/password/drivers/directadmin.php roundcube-1.6.20+dfsg/public_html/plugins/password/drivers/directadmin.php --- roundcube-1.6.19+dfsg/public_html/plugins/password/drivers/directadmin.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/public_html/plugins/password/drivers/directadmin.php 2026-10-19 08:49:43.000000000 +0200 @@ -60,7 +60,7 @@ } if (isset($response['error']) && $response['error'] == 1) { - return ['code' => PASSWORD_ERROR, 'message' => strip_tags($response['text'])]; + return ['code' => PASSWORD_ERROR, 'message' => $response['text']]; } return PASSWORD_SUCCESS; diff -Nru roundcube-1.6.19+dfsg/public_html/plugins/password/drivers/modoboa.php roundcube-1.6.20+dfsg/public_html/plugins/password/drivers/modoboa.php --- roundcube-1.6.19+dfsg/public_html/plugins/password/drivers/modoboa.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/public_html/plugins/password/drivers/modoboa.php 2026-10-19 08:49:43.000000000 +0200 @@ -82,20 +82,35 @@ } // Get user ID (pk) - $userid = $decoded[0]->pk; + $userid = null; + $user = null; + foreach ($decoded as $account) { + if (isset($account->username) && $account->username === $username) { + $userid = $account->pk; + $user = [ + 'username' => $account->username, + 'mailbox' => $account->mailbox, + 'role' => $account->role, + ]; + break; + } + } + + if ($user === null) { + return PASSWORD_CONNECT_ERROR; + } // Encode json with new password - $ret['username'] = $decoded[0]->username; - $ret['mailbox'] = $decoded[0]->mailbox; - $ret['role'] = $decoded[0]->role; - $ret['password'] = $passwd; // new password - $encoded = json_encode($ret); + $user['password'] = $passwd; + $encoded = json_encode($user); + + $url = "https://{$host}/api/v1/accounts/{$userid}/"; // Call HTTP API Modoboa $curl = curl_init(); curl_setopt_array($curl, [ - CURLOPT_URL => "https://" . $host . "/api/v1/accounts/" . $userid . "/", + CURLOPT_URL => "https://{$host}/api/v1/accounts/{$userid}/", CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, diff -Nru roundcube-1.6.19+dfsg/public_html/plugins/password/password.php roundcube-1.6.20+dfsg/public_html/plugins/password/password.php --- roundcube-1.6.19+dfsg/public_html/plugins/password/password.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/public_html/plugins/password/password.php 2026-10-19 08:49:43.000000000 +0200 @@ -425,7 +425,7 @@ } if ($message) { - $reason .= ' ' . $message; + $reason .= ' ' . rcube::Q($message, 'strict', false); } return $reason; diff -Nru roundcube-1.6.19+dfsg/public_html/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php roundcube-1.6.20+dfsg/public_html/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php --- roundcube-1.6.19+dfsg/public_html/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/public_html/plugins/squirrelmail_usercopy/squirrelmail_usercopy.php 2026-10-19 08:49:43.000000000 +0200 @@ -170,7 +170,10 @@ $rcmail = rcmail::get_instance(); /**** File based backend ****/ - if ($rcmail->config->get('squirrelmail_driver') == 'file' && ($srcdir = $rcmail->config->get('squirrelmail_data_dir'))) { + if ($rcmail->config->get('squirrelmail_driver') == 'file' + && ($srcdir = $rcmail->config->get('squirrelmail_data_dir')) + && strpos($uname, '/') === false // security check + ) { if (($hash_level = $rcmail->config->get('squirrelmail_data_dir_hash_level')) > 0) { $srcdir = slashify($srcdir).chunk_split(substr(base_convert(crc32($uname), 10, 16), 0, $hash_level), 1, '/'); } diff -Nru roundcube-1.6.19+dfsg/tests/Framework/Mime.php roundcube-1.6.20+dfsg/tests/Framework/Mime.php --- roundcube-1.6.19+dfsg/tests/Framework/Mime.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/tests/Framework/Mime.php 2026-10-19 08:49:43.000000000 +0200 @@ -136,6 +136,21 @@ } /** + * Test sanitization of decoded header values + * Uses rcube_mime::decode_address_list() + */ + public function test_decode_address_list_sanitization() + { + $content = "Attacker\r\nX-Injected-By: test\r\nX-Pad: p"; + $input = '=?UTF-8?B?' . base64_encode($content) . '?= <[email protected]>'; + $result = \rcube_mime::decode_address_list($input); + + $content = str_replace("\r\n", ' ', $content); + $this->assertSame($content, $result[1]['name']); + $this->assertSame("\"{$content}\" <[email protected]>", $result[1]['string']); + } + + /** * Test decoding of header values * Uses rcube_mime::decode_mime_string() */ diff -Nru roundcube-1.6.19+dfsg/tests/Framework/Utils.php roundcube-1.6.20+dfsg/tests/Framework/Utils.php --- roundcube-1.6.19+dfsg/tests/Framework/Utils.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/tests/Framework/Utils.php 2026-10-19 08:49:43.000000000 +0200 @@ -321,6 +321,23 @@ $this->assertSame('#rcmbody * { background-color: silver; }', $mod); $mod = \rcube_utils::mod_css_styles('//* test */* {background-color: silver;}', 'rcmbody', true); $this->assertSame('/* evil! */', $mod); + + // CSS at-rules + $style = '@layer a; div { display: none; }'; + $mod = \rcube_utils::mod_css_styles($style, 'rcmbody', true); + $this->assertSame('@layer a; #rcmbody div { display: none; }', $mod); + $style = '@layer a, b; div { display: none; }'; + $mod = \rcube_utils::mod_css_styles($style, 'rcmbody', true); + $this->assertSame('@layer a, b; #rcmbody div { display: none; }', $mod); + $style = '@layer a { div { display: none; } }'; + $mod = \rcube_utils::mod_css_styles($style, 'rcmbody', true); + $this->assertSame('@layer a { #rcmbody div { display: none; } }', $mod); + $style = '@media screen { @layer a; div { display: none; } }'; + $mod = \rcube_utils::mod_css_styles($style, 'rcmbody', true); + $this->assertSame('@media screen { @layer a; #rcmbody div { display: none; } }', $mod); + $style = '::before { color: red; }'; + $mod = \rcube_utils::mod_css_styles($style, 'rcmbody', true); + $this->assertSame('#rcmbody ::before { color: red; }', $mod); } /** @@ -611,6 +628,11 @@ ['http://[::ffff:7f00:1]/', true], ['http://[0:0:0:0:0:ffff:7f00:1]/', true], ['http://[::ffff:a9fe:a9fe]/', true], + ['http://[::]/', true], + ['http://[64:ff9b::]/', true], + ['http://[64:ff9b:1::]/', true], + ['http://[64:ff9b::a9fe:a9fe]/', true], + ['http://[64:ff9b::7f00:1]/', true], // Non-local hosts ['http://[2001:470::76:0:0:0:2]', false], ['http://domain.tld', false], diff -Nru roundcube-1.6.19+dfsg/tests/Framework/Washtml.php roundcube-1.6.20+dfsg/tests/Framework/Washtml.php --- roundcube-1.6.19+dfsg/tests/Framework/Washtml.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/tests/Framework/Washtml.php 2026-10-19 08:49:43.000000000 +0200 @@ -565,6 +565,55 @@ '<svg><image><set attributeName="src" to="http://attacker.example/d"/></image></svg>', '<svg><image><set attributeName="src" x-washed="to" /></image></svg>', ], + [ + '<svg><image><animate attributeName="src" by="http://attacker.example/a"/></image></svg>', + '<svg><image><animate attributeName="src" x-washed="by" /></image></svg>', + ], + [ + '<svg><image><animate attributeName="fill" by="url(http://attacker.example/a)"/></image></svg>', + '<svg><image><!-- animate blocked --></image></svg>', + ], + [ + // FuncIRI smuggled through 'by' on a presentation attribute + '<svg><rect><animate attributeName="fill" by="url(http://attacker.example/f)" dur="1s"/></rect></svg>', + '<svg><rect><!-- animate blocked --></rect></svg>', + ], + + [ + // FuncIRI smuggled through 'from' + '<svg><rect><animate attributeName="fill" from="url(http://attacker.example/g)" dur="1s"/></rect></svg>', + '<svg><rect><!-- animate blocked --></rect></svg>', + ], + [ + // Hex escape terminated by a space (CSS Syntax L3, 4.3.7) + '<svg><rect fill="u\72 l(http://attacker.example/h)"/></svg>', + '<svg><rect x-washed="fill" /></svg>', + ], + [ + // Zero-padded hex escape + '<svg><rect fill="\000075rl(http://attacker.example/i)"/></svg>', + '<svg><rect x-washed="fill" /></svg>', + ], + [ + // Literal escape: backslash before a non-hex character + '<svg><rect fill="u\rl(http://attacker.example/j)"/></svg>', + '<svg><rect x-washed="fill" /></svg>', + ], + [ + // Several literal escapes combined + '<svg><rect fill="\u\r\l(http://attacker.example/k)"/></svg>', + '<svg><rect x-washed="fill" /></svg>', + ], + [ + // Escaped url() smuggled through a SMIL 'by' attribute + '<svg><rect><animate attributeName="fill" by="u\rl(http://attacker.example/l)" dur="1s"/></rect></svg>', + '<svg><rect><!-- animate blocked --></rect></svg>', + ], + [ + // An escape that does NOT spell url() must not be over-blocked + '<svg><rect fill="\23 ff0000"/></svg>', + '<svg><rect fill="#ff0000" /></svg>', + ], ]; } diff -Nru roundcube-1.6.19+dfsg/tests/Rcmail/Sendmail.php roundcube-1.6.20+dfsg/tests/Rcmail/Sendmail.php --- roundcube-1.6.19+dfsg/tests/Rcmail/Sendmail.php 2026-09-06 10:27:09.000000000 +0200 +++ roundcube-1.6.20+dfsg/tests/Rcmail/Sendmail.php 2026-10-19 08:49:43.000000000 +0200 @@ -91,7 +91,25 @@ */ function test_extract_inline_images() { - $this->markTestIncomplete(); + $message = new \Mail_mime("\r\n"); + $message->setHTMLBody('<body></body>'); + \rcmail_sendmail::extract_inline_images($message, '[email protected]'); + $this->assertSame('<body></body>', $message->getMessageBody()); + + $message = new \Mail_mime("\r\n"); + $ctype = "test/test\r\nX-Inject:test"; + $b64 = base64_encode(file_get_contents(__DIR__ . '/../src/logo.jpg')); + $message->setHTMLBody('<body><img title="" src="data:' . $ctype . ';base64,' . $b64 . '"></body>'); + \rcmail_sendmail::extract_inline_images($message, '[email protected]'); + + $body = $message->getMessageBody(); + $this->assertStringContainsString('Content-Type: image/jpeg', $body); + $this->assertStringContainsString('Content-Disposition: inline', $body); + $this->assertStringContainsString('size=10549', $body); + $this->assertStringContainsString(substr($b64, 0, 32), $body); + $this->assertMatchesRegularExpression('/name=[a-f0-9]+\.jpeg/', $body); + $this->assertMatchesRegularExpression('/<img title=3D"" src=3D"cid:[a-f0-9]+@test\.com"/', $body); + $this->assertMatchesRegularExpression('/Content-ID: <[a-f0-9]+@test\.com>/', $body); } /** Binary files /tmp/BuR1VjgKj9/roundcube-1.6.19+dfsg/tests/src/logo.jpg and /tmp/0VrCIaFpiI/roundcube-1.6.20+dfsg/tests/src/logo.jpg differ
signature.asc
Description: PGP signature

