From 27ec6cc9cb25e1ef8b4d4ef39ce76d619caa6870 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Wed, 18 Mar 2026 10:35:16 +0100 Subject: [PATCH] Fix SSRF + Information Disclosure via stylesheet links to a local network hosts Reported by Georgios Tsimpidas (aka Frey), Security Researcher at https://i0.rs/ --- CHANGELOG.md | 1 + composer.json-dist | 3 +- program/actions/mail/index.php | 2 +- program/actions/utils/modcss.php | 2 +- program/lib/Roundcube/rcube_utils.php | 46 ++++++++++++++++++++++++- program/lib/Roundcube/rcube_washtml.php | 2 +- tests/Framework/Utils.php | 34 ++++++++++++++++++ 7 files changed, 85 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b0f3182c..86afc96bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Security: Fix remote image blocking bypass via a crafted body background attribute - Security: Fix fixed position mitigation bypass via use of !important - Security: Fix XSS issue in a HTML attachment preview +- Security: Fix SSRF + Information Disclosure via stylesheet links to a local network hosts ## Release 1.6.13 diff --git a/composer.json-dist b/composer.json-dist index c8cf3f738..b9140e3b8 100644 --- a/composer.json-dist +++ b/composer.json-dist @@ -20,7 +20,8 @@ "roundcube/rtf-html-php": "~2.1", "masterminds/html5": "~2.7.0", "bacon/bacon-qr-code": "^2.0.0", - "guzzlehttp/guzzle": "^7.3.0" + "guzzlehttp/guzzle": "^7.3.0", + "mlocati/ip-lib": "^1.22.0" }, "require-dev": { "phpunit/phpunit": "^9" diff --git a/program/actions/mail/index.php b/program/actions/mail/index.php index f72c3f537..6bb5c8365 100644 --- a/program/actions/mail/index.php +++ b/program/actions/mail/index.php @@ -1274,7 +1274,7 @@ class rcmail_action_mail_index extends rcmail_action if (isset($attrib['href'])) { $attrib['href'] = preg_replace('/[\x00-\x1F]/', '', $attrib['href']); - if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) { + if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href']) && !rcube_utils::is_local_url($attrib['href'])) { $tempurl = 'tmp-' . md5($attrib['href']) . '.css'; $_SESSION['modcssurls'][$tempurl] = $attrib['href']; $attrib['href'] = $rcmail->url([ diff --git a/program/actions/utils/modcss.php b/program/actions/utils/modcss.php index 28631581f..53d54a7e3 100644 --- a/program/actions/utils/modcss.php +++ b/program/actions/utils/modcss.php @@ -47,7 +47,7 @@ class rcmail_action_utils_modcss extends rcmail_action $ctype = null; try { - $client = rcube::get_instance()->get_http_client(); + $client = rcube::get_instance()->get_http_client(['allow_redirects' => false]); $response = $client->get($realurl); if (!empty($response)) { diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index 5e0ef7da7..5e8ac84cd 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -1,6 +1,8 @@ contains($address)) { + return true; + } + } + + return false; + } + + // FIXME: Should we accept any non-fqdn hostnames? + return (bool) preg_match('/^localhost(\.localdomain)?$/i', $host); + } + + return false; + } + /** * Replace all css definitions with #container [def] * and remove css-inlined scripting, make position style safe diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php index 052472e8e..f9ee8d513 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -393,7 +393,7 @@ class rcube_washtml } if (preg_match('/^(http|https|ftp):.+/i', $uri)) { - if (!empty($this->config['allow_remote'])) { + if (!empty($this->config['allow_remote']) || rcube_utils::is_local_url($uri)) { return $uri; } diff --git a/tests/Framework/Utils.php b/tests/Framework/Utils.php index 66fd20e9a..3baa8611f 100644 --- a/tests/Framework/Utils.php +++ b/tests/Framework/Utils.php @@ -556,6 +556,40 @@ class Framework_Utils extends PHPUnit\Framework\TestCase } } + /** + * Test is_local_url() + * + * @dataProvider provide_is_local_url_cases + */ + #[DataProvider('provide_is_local_url_cases')] + public function test_is_local_url($input, $output) + { + $this->assertSame($output, \rcube_utils::is_local_url($input)); + } + + /** + * Test-Cases for is_local_url() test + */ + public static function provide_is_local_url_cases(): iterable + { + return [ + // Local hosts + ['https://127.0.0.1', true], + ['https://10.1.1.1', true], + ['https://172.16.0.1', true], + ['https://192.168.0.100', true], + ['https://169.254.0.200', true], + ['http://[fc00::1]', true], + ['ftp://[::1]:8080', true], + ['//127.0.0.1', true], + ['http://localhost', true], + ['http://localhost.localdomain', true], + // Non-local hosts + ['http://[2001:470::76:0:0:0:2]', false], + ['http://domain.tld', false], + ]; + } + /** * rcube:utils::strtotime() */