PR 本記事には広告(Amazonアソシエイト・もしもアフィリエイト・A8.net等)が含まれます。掲載情報の正確性には努めていますが、商品の詳細は必ずリンク先で最新情報をご確認ください。
CTF・セキュリティ学習

【Webキャッシュ汚染編】CDNを毒で塗る攻撃と守り方|CTF思考フレームワーク #19

かも次郎とアンペンが「Webキャッシュ汚染」を解説するマスコットイラスト
安全に生きたい編集部

こんにちは、アンペンです!

前回は、中継装置の境界解釈ズレを突くHTTPリクエストスマグリングを扱いました。

今回は、CDNやプロキシのキャッシュに悪意ある応答を保存させるWebキャッシュ汚染を見ていきます。1人を汚染すれば、後続の利用者にまでまとめて配られる、影響範囲の広い攻撃です。

キャッシュって、サイトを速くするための仕組みでしょ?それで攻撃に?

キャッシュは『あるリクエストへの応答』を、後続の同じリクエストにも返す仕組み。その応答に毒が混ざると、後続の利用者全員に配られてしまうんだ。

前回のスマグリングに続いて、今日も“裏方のインフラ”が舞台です。テーマは、サイトを速くするための縁の下の力持ち——キャッシュ。普段はありがたい仕組みなのに、ひとたび“毒”を覚え込ませると、それを後から来た利用者みんなに律儀に配ってしまう。たった一度の攻撃が、何千人にも届く。今日はその影響範囲の広さと、防ぎ方を見ていきましょう。

まず結論

Webキャッシュ汚染は、キャッシュキーに含まれない入力(Hostヘッダや独自ヘッダなど)が応答に反射されるとき、汚染された応答がキャッシュされて後続ユーザーに配られる攻撃です。キャッシュキー設計と「unkeyed入力を信用しない」が守りの中心です。

この記事で分かること

  • キャッシュキーとunkeyed入力の関係
  • キャッシュ汚染が広く配信されてしまう理由
  • キャッシュキー設計とヘッダ正規化による守り方
難易度:中級向け 所要時間:11分 体験:キャッシュキーを意識する おすすめ:#18の後

📖 はじめてのWebセキュリティ #19|Webキャッシュ汚染編
『速くする仕組み』のキャッシュが、汚染で被害を広げる入口になる構造を学びます。 シリーズ一覧を見る →

⚠️ 大事なお約束
この記事の確認は、CTF・公式ラボ・自分で作った検証環境だけで行ってください。実在のサービスのキャッシュに対する汚染テストは、業務妨害や不正アクセスに該当する可能性があります。

キャッシュは「キー」で結果を覚える

CDNやリバプロのキャッシュは、リクエストの一部を「キャッシュキー」として記憶し、同じキーの後続リクエストには保存しておいた応答を返します。通常はURLパスやクエリだけがキーになります。

問題は、キャッシュキーに含まれない入力(例: 一部のリクエストヘッダ)を、サーバーが応答に反射するときです。攻撃者が一度だけ悪意ある入力を送れば、汚染された応答がキャッシュに保存され、その後同じURLにアクセスする利用者全員に配られてしまいます。

ここに落とし穴があります。キャッシュは“URLが同じなら中身も同じ”と思い込んでいる。でも実際には、URL以外の要素——たとえば一部のリクエストヘッダ——が答えを変えてしまうことがあります。すると、攻撃者が一度だけ毒入りヘッダを送って“毒入りの模範解答”を貼らせれば、あとは同じURLに来た人全員に、その毒入り解答が配られ続ける。一発仕込めば、あとは自動で拡散——これがキャッシュ汚染のこわさです。

まず『キャッシュ』のおさらいから。キャッシュは、よく聞かれる質問に対する“模範解答”を手元に貼っておいて、同じ質問が来たら毎回サーバに聞かずに即答する仕組みです。このとき『どの質問か』を見分ける目印を『キャッシュキー』と呼びます。ふつうはURL(住所)だけがキーになる。つまり「同じURLなら、同じ答えでいいよね」という前提で動いているんです。

図解:通常キャッシュ vs 汚染キャッシュ

通常のキャッシュは正常な応答を共有するが、汚染されたキャッシュは1回の汚染が後続ユーザー全員に配られる比較図
キャッシュキーに含まれない入力が応答に反射されると、毒が後続の利用者に共有されてしまう。

普段のキャッシュは便利な高速化ですが、unkeyed入力が反射されると、CDNが意図せず「攻撃者の応答を全員に配る配信装置」になります。

自販機の取り出し口に偽物の商品が置かれ、後から来た利用者がそれを本物と思って受け取ってしまうたとえ図
置かれた偽物を後の人が本物と思って受け取る。CDNキャッシュも同じで、汚染された応答が後続に配られる。
🥤 たとえるなら、自販機の取り出し口の置き引き

自販機の取り出し口に「売り切れですが代わりにこれをどうぞ」と書かれた紙と偽の缶が置かれていたら、後から来た人がそれを本物だと思って受け取ってしまうかもしれません。CDNのキャッシュも同じで、最初に置かれた応答を後続の利用者がそのまま受け取ります。最初の応答が毒入りだと、被害が一気に広がります。

ここで覚える用語:unkeyed入力
キャッシュキーに含まれないが、応答内容には影響を与える入力(リクエストヘッダなど)のことです。 X-Forwarded-HostX-Original-URL のようなヘッダが応答に反射されるケースが典型です。

『unkeyed入力』という言葉、ここが今日の肝なので、ゆっくり。要は『答えには影響するのに、目印(キー)には数えられていない入力』のことです。たとえるなら、料理の注文で“辛さの指定”を聞いておきながら、伝票には料理名しか書かない店。次の人が同じ料理を頼むと、前の人の“激辛”がそのまま出てきてしまう。答えを左右する要素は、必ず目印にも入れる——このズレをなくすことが、すべての出発点になります。

代表的なキャッシュ汚染3パターン

汚染の入口は、代表的に3つ。『Hostっぽいヘッダがリンクに反射される』『大文字小文字や順序の違いを同じURLとみなしてしまう』『JSONやJSの中に入力が反射される』。細部は違っても、共通するのは“答えを変える入力が、キーに数えられていない”という一点です。

よくある汚染経路

Hostヘッダ汚染・クエリ正規化の差・JSON/JS反射の3つの代表的なキャッシュ汚染パターンを示したカード型インフォグラフィック
①Hostヘッダ汚染 ②クエリ正規化の差 ③JSON/JS反射。守りは応答影響入力をキー/Varyに含めること。
  • Host系ヘッダ汚染:X-Forwarded-HostHost をアプリが応答(リンクURL等)に埋め込み、キャッシュキーには含まないケース
  • クエリ正規化の差:同じURLでも大文字小文字や順序が違うとサーバ応答は変わるのに、キャッシュは同一キーとして扱う
  • JSON/JS応答の反射:JSONPやJSファイル内に callbackreferrer が反射され、汚染版がCDNに保存される

これらは、CDNやLB側のキャッシュキー設定とアプリ側の応答生成ロジックの「ズレ」が原因です。1か所が悪いというより、運用全体での見直しが必要になります。

ここで大事な視点を一つ。キャッシュ汚染は、『どこか1か所のバグ』というより、“インフラとアプリの連携ミス”から生まれます。CDN(インフラ)は「URLが同じだからキャッシュしていいや」と判断し、アプリは「ヘッダの値をリンクに埋め込もう」と動く。それぞれは正しく仕事をしているのに、二人の前提がズレているせいで穴が開く。だから直すときも、インフラ担当とアプリ担当が“同じ地図”を見て合わせる必要があるんです。

自分のラボで確かめるなら、ねらいは『何がキャッシュの目印になっているか』を整理することです。攻撃を成功させる必要はありません。X-CacheAge といった応答ヘッダを見れば、自分のリクエストがキャッシュから来たのかが分かります。本物のサービスのキャッシュに毒を送り込むのは、多数の人を巻き込む業務妨害になりかねないので、絶対にやめてください。

CTFでやってみよう:キャッシュキーを意識して読む

やってみよう / 演習環境限定

自分のラボで、CDN/プロキシのキャッシュキー設定を確認しよう

目的は実際に汚染することではなく、「自分の環境で何がキーに使われているか」を整理することです。

  1. CTFや自分の検証環境のCDN/プロキシで、キャッシュキー設定を確認する(パス・クエリ・どのヘッダを使うか)
  2. サーバ応答内で利用者リクエストの値を「反射」している箇所(URL生成・リンク生成)を探す
  3. 反射対象がキャッシュキーに含まれているかを照合する
  4. X-Cache や Age ヘッダから、自分のリクエストがキャッシュにヒットしたかを観察する
  5. Vary ヘッダで応答差分を分けられる項目を整理する
他人のサービスに対する汚染試行は絶対にやめてください。確認は自分のラボの範囲だけです。

守りの考え方は、unkeyedの説明を逆さにするだけです。

答えを変える入力を、ぜんぶ目印(キー)に入れればいいの?

その通り。答えを左右するものは、必ず目印(キャッシュキー)に含める——これが大原則だよ。ヘッダで答えが変わるなら Vary ヘッダで「この項目ごとに別々に覚えてね」とキャッシュに伝える。さらに、そもそも信用ならない独自ヘッダ(X-Forwarded-* など)は、アプリ側で鵜呑みにしない。『反射するなら、キーに入れる』。この一言を合言葉にすると、設計がブレないよ。

守る側なら、「キーと応答を一致させる」が基本

キャッシュ汚染の守りは、「応答に影響する入力は、必ずキャッシュキーに含める」「unkeyed入力を応答に反射しない」の2点が中心です。

守るための基本チェック
  • 応答に影響する全ての入力を、キャッシュキーまたは Vary ヘッダで分離する
  • 独自ヘッダ( X-Forwarded-* 等)はアプリ側で信用せず、ホワイトリストの送信元から来た値だけ扱う
  • URL正規化(大文字小文字、末尾スラッシュ、クエリ順序)をCDN側で揃える
  • 個人化された応答( Set-Cookie や認証必須コンテンツ)は明示的にno-cacheする
  • JSONP・JS応答の動的部分は、可能ならキャッシュ対象から外す
  • X-Cache・Age・CDN固有ログで、汚染兆候を継続監視する

『キーと応答が一致してないと、毒が広く回る』ってわかりやすいね。

そう。キャッシュ設計はインフラとアプリの両方で揃えるのが大事だよ。

ここまでをひと言で言うと、キャッシュ汚染対策は『答えを変えるものは、必ず目印に数える』。スマグリングと同じで、これも“インフラとアプリの足並み”の問題です。速くする仕組みは手放せませんが、「何を同じとみなすか」の前提を両者でそろえておけば、毒入りの模範解答が貼られる隙はなくなります。

まとめ:『反射するならキーに入れる』

今回のポイント
  • キャッシュ汚染はunkeyed入力の応答反射で成立する
  • 影響範囲が広く、1人の汚染で多数の利用者に配信される
  • 守りは応答影響のある入力をキャッシュキー/Varyに含める
  • 独自ヘッダはアプリで信用しない、URL正規化をCDN側で揃える

今日の持ち帰りは『反射するなら、キーに入れる』です。キャッシュは強力な味方ですが、“同じURLなら同じ答え”という思い込みが崩れた瞬間、毒の拡散装置に早変わりします。答えを左右する入力を残らず目印に含める——その当たり前を徹底すれば、一度の汚染が大勢に広がる事故を防げます。

次回は、生成AIをアプリに組み込んだときのWeb LLMのセキュリティを扱います。プロンプトインジェクションと統合アプリの新攻撃面を見ていきましょう。

次に読みたい記事

参考資料

記事URLをコピーしました