Webキャッシュポイズニングの仕組みと対策。汚染された応答が正規利用者へ配信される欠陥を塞ぐ
対象の目安: Webアプリ開発、インフラ運用、実務

CDNやリバースプロキシによるキャッシュは、同じ応答を複数の利用者へ配って表示を速くする仕組みです。ここで、キャッシュがリクエストを見分けるときに参照しない入力の値が応答に反映されると、攻撃者はその入力を細工して汚染した応答を作り、キャッシュに載せられます。載った汚染応答は、同じキャッシュキーを持つ後続のリクエストにそのまま配られるため、無関係な正規利用者へ届きます。これがWebキャッシュポイズニングです。1回の細工した応答が多数の利用者へ広がる増幅効果を持つ点が、この欠陥の重さの理由です。
この記事はWebアプリの開発者とインフラ運用の担当者を対象に、キャッシュキーとレスポンス反映の関係からポイズニングが成立する機構を追い、キャッシュ設計を軸にした防御を整理します。攻撃を再現する具体的なペイロードは示しません。挙動を確かめる検証は、自分が管理する環境か、明示的に許可された環境に対してのみ行う前提です。許可のない検証は不正アクセス禁止法などの法令や利用契約に触れうるため行いません。記述はPortSwigger Web Security Academy、HTTPキャッシュを定めるRFC 9111、MDNのVaryおよびCache-Controlの各ドキュメントにもとづきます。
Webキャッシュポイズニングとは何か
Webキャッシュポイズニングは、キャッシュに悪意ある応答を保存させ、それを後続の利用者へ配信させる欠陥です。攻撃者は自分のリクエストで汚染した応答を1度キャッシュに載せるだけで、以降そのキャッシュエントリに一致するリクエストを送る利用者全員へ、同じ汚染応答が配られます。攻撃者と利用者が同じ通信をしなくても被害が及ぶ点で、反射型の欠陥より影響範囲が広がります。
成立には二つの条件がそろう必要があります。一つは、キャッシュが見分けに使わない入力の値が、応答の中身に影響すること。もう一つは、その汚染された応答がキャッシュに保存され、他の利用者へ再利用されることです。前者はアプリの実装、後者はキャッシュの設定に関わるため、対策も両面から組み立てます。
キャッシュキーとunkeyed inputの関係
キャッシュがリクエストを同じものとして扱うかどうかは、リクエスト全体ではなく、あらかじめ決めた一部分の一致だけで判断されます。RFC 9111はこの判断材料をキャッシュキーと呼び、最小の構成としてリクエストメソッドと対象URIを挙げています。実装によってはURIだけをキーにするものもあります。
ここで押さえておく点は、キャッシュキーに含まれない入力が値の違いによらず同じリクエストとして扱われることです。たとえば多くのヘッダは通常キャッシュキーの対象外です。クエリ文字列は対象URIの一部として通常はキーに含まれますが、CDNでの除外設定や正規化、特定パラメータの無視といった構成ではキー外に置かれることがあります。こうしてキーに含まれない入力はunkeyed inputと呼ばれます。キャッシュから見れば無視される入力ですが、背後のアプリはこれらを読んで応答を組み立てることがあります。キャッシュが無視する入力をアプリが応答に反映するというこのずれが、ポイズニングの土台になります。
レスポンス反映が汚染に変わる機構
汚染が起きる流れを、入力から配信まで順に追うと理解しやすくなります。
まず攻撃者は、キャッシュキーに含まれないヘッダやクエリに細工した値を入れてリクエストを送ります。代表例がX-Forwarded-Hostのようなヘッダで、アプリが応答内のURL生成にこの値を使う実装だと、細工したホスト名が応答のリンクやスクリプトの読み込み先に反映されます。次に、キャッシュはこのリクエストのキャッシュキーが通常のものと同じであるため、返ってきた汚染応答を正当な応答として保存します。最後に、別の正規利用者が同じキャッシュキーになるリクエストを送ると、キャッシュは保存済みの汚染応答を返します。利用者は正規のURLへアクセスしているのに、攻撃者が仕込んだ内容を受け取ります。
反映される先によって被害は変わります。応答内のスクリプト読み込み先が攻撃者の指すホストに書き換われば、閲覧した利用者のブラウザで任意のスクリプトが動く事態につながります。この意味でキャッシュポイズニングは、反射だけでは攻撃者自身にしか届かない反映を、キャッシュを介して他人へ配る増幅装置として働きます。
テストで使われるcache buster
キャッシュの挙動を安全に調べるために、検証ではcache busterという手法が使われます。これはリクエストごとに一意な値を持つパラメータを足して、キャッシュキーを他と重複させない工夫です。キャッシュキーが一意になると、そのリクエストの応答は検証者自身にしか配られなくなります。
cache busterを使う目的は、汚染応答を実際の利用者へ配ってしまう事故を避けることにあります。unkeyed inputが応答に反映されるかどうかを確かめる段階では、まだ本番のキャッシュエントリを汚したくありません。一意なキャッシュキーの下で反映の有無だけを確認し、影響を自分の範囲に閉じ込めるための安全弁がcache busterです。ただしcache busterが隔離として働くのは、加えたパラメータが実際にキャッシュキーへ含まれる場合に限ります。パラメータがキーから除外される設定では応答が一意に切り分けられず、本番のキャッシュエントリを汚しかねません。そのため、複数の値でキャッシュのヒット状況を見て、加えたパラメータがキーに効いていることを先に確かめます。cache busterが機能することは、別のunkeyed inputが存在することや、その入力が応答へ影響することまでは証明しない点にも注意します。
キャッシュとバックエンドの解釈のずれという点では、HTTPリクエストスマグリングも同じ根から生じる欠陥です。中継機器ごとのリクエスト境界の読み違いがキャッシュ汚染にもつながる仕組みは次の記事で整理しています。
あわせて読みたい
HTTPリクエストスマグリングの仕組みと対策。フロントとバックの境界解釈のズレを塞ぐ
Webキャッシュ欺瞞との違い
キャッシュを狙う欠陥には、ポイズニングと向きが逆のWebキャッシュ欺瞞(web cache deception)があります。ポイズニングが攻撃者の汚染応答を他人へ配る欠陥であるのに対し、欺瞞は他人の機密応答をキャッシュに載せて攻撃者が奪う欠陥です。
欺瞞が成立するのは、キャッシュとアプリサーバでURLの解釈が食い違うときです。キャッシュルールは、拡張子が.cssや.jsで終わるパスや、/staticで始まるパスを静的資産とみなして保存するといった、URLパス上の文字列で決められることが多くあります。攻撃者は、この解釈差を突いて機密ページのURLを静的資産に見えるよう細工します。被害者がその細工URLへアクセスすると、本来キャッシュすべきでない被害者向けの動的応答がキャッシュに保存されます。この応答が認証状態などで分離されず、攻撃者が同じキャッシュキーで再現できる場合に、攻撃者は同じURLを叩いて保存された中身を取り出せます。奪われる対象がセッショントークンやAPIキーであれば、アカウント乗っ取りに至ります。
二つは向きが逆ですが、原因は共通してキャッシュの見分け方とアプリの応答生成のずれにあります。したがって対策も、何をキャッシュキーにするか、何をキャッシュしてよいかという設計の見直しで重なります。
キャッシュキーとVaryによる分離
防御の中心は、応答に影響する入力を正しくキャッシュキーへ反映させ、影響しない入力を応答に持ち込まないことです。応答内容を変える要素がキャッシュキーに含まれていれば、値が違うリクエストには別のキャッシュエントリが割り当てられ、汚染応答が無関係な利用者へ配られなくなります。
HTTPにはこの分離を宣言する仕組みとしてVaryヘッダがあります。VaryはメソッドとURI以外のどのリクエストヘッダが応答内容に影響したかを示し、キャッシュに対してそのヘッダごとに別のエントリを持つよう指示します。応答がAccept-Languageなどのヘッダで変わるなら、それをVaryに列挙することで、ヘッダの値が異なる利用者には別の応答が配られます。ただしVaryはリクエストヘッダを対象とする仕組みで、キャッシュキーに含めるべき入力を過不足なく指定する設計判断は実装側の責任として残ります。
MDNは、Varyヘッダを含めることで、指定したヘッダごとに応答が別々にキャッシュされることを保証すると説明し、コンテンツネゴシエーション時のキャッシュキー生成に用いられるとしています。
キャッシュキーの設計では、PortSwiggerが挙げるように、危険なヘッダをそもそも受け付けない、あるいはキャッシュへ渡す前にリクエストを書き換えて正規化する方向が有効です。X-Forwarded-Hostのような反映されやすいヘッダを、キャッシュ層で除去または固定値に書き換えれば、アプリがそれを応答へ反映しても攻撃者が制御できなくなります。
Cache-Controlとキャッシュ対象の限定
もう一つの軸は、そもそも何をキャッシュに載せるかを絞ることです。利用者ごとに変わる動的な応答や機密を含む応答は、共有キャッシュに載せない方針が安全です。ここでCache-Controlヘッダが効きます。
MDNの整理では、no-storeはあらゆる種類のキャッシュに応答を保存させないディレクティブで、privateは応答を私的キャッシュにのみ保存させ、CDNやプロキシのような共有キャッシュへの保存を防ぐディレクティブです。ログイン後の応答やCookieで管理するセッションに紐づく応答には、privateを付けて共有キャッシュから外すことが推奨されています。動的応答を共有キャッシュの対象から外しておけば、欺瞞で機密が保存される経路も、ポイズニングで動的ページが汚染される経路も同時に狭まります。
防御の実装手順
キャッシュポイズニングと欺瞞への防御は、アプリ側の実装とキャッシュ層の設定を組み合わせて段階的に組み立てます。
- 1
アプリが応答生成に使う入力を洗い出し、X-Forwarded-Hostなどキャッシュキー外のヘッダやクエリを応答へ反映していないか点検する
- 2
応答生成に使わない不要な入力や信頼できない経路のヘッダは、キャッシュへ渡す前にキャッシュ層で除去または固定値へ書き換える(転送系ヘッダは信頼済みプロキシが付けた値だけを採用する)
- 3
正当に応答内容を変えるリクエストヘッダはVaryへ正しく列挙し、値ごとにキャッシュエントリを分離する
- 4
利用者ごとに変わる応答や機密を含む応答にはCache-Controlのprivateまたはno-storeを付け、共有キャッシュの対象から外す
- 5
CDNやリバースプロキシのキャッシュルールを見直し、拡張子やパス接頭辞だけで静的資産と判定する緩い条件を絞る
- 6
キャッシュとバックエンドでURLやヘッダの解釈が食い違わないよう、両者のパースの前提をそろえる
キャッシュ層での書き換えは、アプリを直せない場合でも攻撃者の制御を断つ実務的な手当てになります。VaryとCache-Controlは、キャッシュに対して分離と非保存を宣言する言語であり、アプリの応答生成が正しいことと合わせて初めて効きます。CDN設定では、静的資産と動的応答の境界を明確にし、動的応答が静的判定に紛れ込まないようにします。
WAFは、キャッシュ層の手前で不審なヘッダやパターンを弾く補助線として使えます。ただしWAFはキャッシュキー設計そのものを代替しないため、正規化と分離を主とした構成に足す位置づけで考えます。WAFの役割と限界は次の記事で整理しています。
あわせて読みたい
WAFの役割と限界、正しい使い方。シグネチャと振る舞い、回避される前提、根本対策との補完
テストと監視
防御を入れたあとは、キャッシュ経由で意図しない反映が残っていないかを確かめます。検証では前述のcache busterを用い、キャッシュキーを一意にした状態でunkeyed inputが応答へ反映されるかを確認します。反映が見つかった場合は、その入力がキャッシュキーに含まれているか、あるいはキャッシュ層で正規化されているかを点検します。検証は自分が管理する環境か許可された環境に限り、本番のキャッシュエントリを汚さないよう一意なキャッシュキーの下で行います。
運用では、共有キャッシュに載る応答のCache-ControlとVaryを定期的に確認し、動的応答が共有キャッシュへ紛れていないかを見ます。キャッシュヒット率や配信先の異常な広がりは、汚染エントリが混入した兆候になりえます。応答ヘッダの適切化という点では、キャッシュ制御ヘッダはセキュリティヘッダと同じく、応答の振る舞いをヘッダで宣言して制御する系統に属します。セキュリティヘッダ全体の設計は次の記事で整理しています。
あわせて読みたい
セキュリティHTTPレスポンスヘッダで多層防御を固める方法
対策チェックリスト
キャッシュを扱う構成を点検するときの確認項目を整理します。
- アプリがX-Forwarded-Hostなどキャッシュキー外の入力を応答へ反映していないか点検したか
- 不要または信頼できない入力を、キャッシュ層で除去または固定値へ書き換えているか
- 応答内容に影響するリクエストヘッダをVaryへ正しく列挙し、エントリを分離しているか
- ログイン後や機密を含む動的応答にCache-Controlのprivateまたはno-storeを付けているか
- CDNのキャッシュルールが拡張子やパス接頭辞だけで静的資産と判定する緩い条件になっていないか
- キャッシュとバックエンドでURLやヘッダの解釈が食い違わないよう前提をそろえたか
- cache busterを使い、一意なキャッシュキーの下でunkeyed inputの反映有無を検証したか
- 共有キャッシュに載る応答のCache-ControlとVaryを定期的に確認する運用があるか
Webキャッシュポイズニングは、キャッシュが見分けに使わない入力をアプリが応答へ持ち込むずれから生じ、1度の汚染応答が多数の利用者へ配られる増幅効果を持ちます。近縁のWebキャッシュ欺瞞は向きこそ逆ですが、原因は同じくキャッシュとアプリの解釈のずれにあります。防ぐ要点は、何をキャッシュキーにし何をキャッシュしてよいかを設計で明確にすることです。unkeyed inputを応答に反映しない実装と、VaryおよびCache-Controlによる分離と非保存の宣言、CDN設定の見直しを軸に据えれば、増幅を前提としたこの欠陥を塞げます。
出典・参考
関連する記事
HTTPリクエストスマグリングの仕組みと対策。フロントとバックの境界解釈のズレを塞ぐ
プロキシとバックエンドがHTTPリクエストの境界を別々に解釈することで起きるリクエストスマグリングを、Content-LengthとTransfer-Encodingの矛盾、CL.TEやHTTP/2ダウングレードdesyncの機構、そして防御設計まで開発者向けに整理します。
WAFの役割と限界、正しい使い方。シグネチャと振る舞い、回避される前提、根本対策との補完
WAFが何を守り何を守れないのかを、シグネチャ検知と振る舞い検知の原理から解説します。回避されうる前提を踏まえ、誤検知(過検知/検知漏れ)の扱い、根本対策との補完まで運用目線で整理します。
セキュリティHTTPレスポンスヘッダで多層防御を固める方法
CSPを中心に、HSTSやX-Content-Type-Options、クリックジャッキング対策、Referrer-PolicyといったセキュリティHTTPレスポンスヘッダの役割と設定を、開発者向けに整理します。


