【ITニュース解説】A shared cache decides whether an authenticated page is private, and `Vary` is the header that says so
2026年10月26日に「Dev.to」が公開したITニュース「A shared cache decides whether an authenticated page is private, and `Vary` is the header that says so」について初心者にもわかりやすく解説しています。
ITニュース概要
共有キャッシュ(CDN等)が認証済みページをキャッシュすると、他ユーザーに誤った情報が表示される恐れがある。これを防ぐには、HTTPヘッダーの`Cache-Control`でキャッシュ禁止や個別指定を行い、`Vary`ヘッダーでCookieなどリクエストに応じ内容が変わることを明示することが重要だ。
ITニュース解説
Webアプリケーションのパフォーマンスを向上させるために、リバースプロキシやCDN(コンテンツデリバリーネットワーク)といったキャッシュ層が重要な役割を果たしている。これらはアプリケーションの前に位置し、一度リクエストされたコンテンツのコピーを保存し、同じリクエストが来た際に高速に応答する。例えば、ログインページや画像ファイルのような静的アセットがキャッシュされれば、ユーザーは素早くコンテンツにアクセスでき、アプリケーションサーバーの負荷も軽減される。
しかし、このキャッシュの仕組みが適切に設定されていないと、セキュリティ上の深刻な問題を引き起こす可能性がある。特に、ユーザー固有の情報を含む認証済みページが共有キャッシュに保存されてしまうと、あるユーザーの個人情報が、別の訪問者に対して誤って表示されてしまう「情報漏洩」のリスクが発生するのだ。キャッシュはサーバーからの応答ヘッダーに従って動作するため、これらのヘッダーのレビューが不足していると、このような予期せぬ挙動につながることが多い。
キャッシュがコンテンツを保存するかどうか、そしてどのように提供するかを決定する際には、主に三つの質問を自問している。一つ目は「この応答を保存しても良いか」、二つ目は「保存されたコピーはまだ有効(新鮮)か」、三つ目は「現在のリクエストは、以前に保存したコピーを生成したリクエストと同じか」である。
最初の質問「この応答を保存しても良いか」には、HTTPヘッダーのCache-Controlディレクティブが答える。例えば、no-storeは、いかなる場合でも応答を保存してはならないと指示する。privateは、ブラウザのキャッシュには許可するが、複数のユーザーで共有される中間キャッシュ(リバースプロキシやCDNなど)には保存を禁止する。publicは共有キャッシュにも保存を許可する。max-ageは、そのコンテンツが何秒間新鮮であるかを指定する。これらのディレクティブが全く指定されていない場合、キャッシュは実装に依存する独自の判断基準(ヒューリスティック)でキャッシュすることがあり、これが問題の温床となることがある。
二つ目の質問「保存されたコピーはまだ有効か」も、主にCache-ControlディレクティブとExpiresヘッダーによって決まる。max-ageやs-maxage(共有キャッシュに特化した鮮度指定)は、コンテンツが新鮮である期間を秒単位で明示的に設定する。Expiresヘッダーは、絶対的な有効期限日時を提供する。これらが指定されていない場合、キャッシュは独自のヒューリスティックを適用し、例えば最終更新からの経過時間の一部を鮮度とみなすことがある。この結果、ユーザーがログアウトした後も、古い認証済みページがキャッシュに残り続け、不適切に提供される可能性がある。
三つ目の質問「現在のリクエストは、以前に保存したコピーを生成したリクエストと同じか」には、Varyヘッダーが答える。これはキャッシュに対し、コンテンツの生成にどのリクエストヘッダーが影響したかを伝えるものだ。もし、あるページのコンテンツがユーザーのCookieによって変化する場合、その応答はVary: Cookieというヘッダーを含める必要がある。これにより、キャッシュは異なるCookieを持つリクエストを別々のオブジェクトとして扱い、それぞれのユーザーに合ったコンテンツを保存・提供する。もしVary: Cookieが指定されていないと、キャッシュは一人のユーザーのために生成されたコンテンツを保存し、そのURLへの後続のリクエスト(別のユーザーからのものも含む)に対して、同じ誤ったコンテンツを提供してしまうことになる。
このような設定の不備によって、主に二つの失敗パターンが考えられる。一つ目は、認証済みコンテンツがキャッシュ可能とされてしまうパターンだ。例えば、ログイン済みのユーザー向けにレンダリングされたページが、Cache-Control: public, max-age=300というヘッダーを伴って返されると、共有キャッシュはそれをURLをキーとして保存する。次に、別のユーザーが同じURLにアクセスした場合、キャッシュは保存されたコンテンツを返してしまい、アプリケーションサーバーは二度目のリクエストを受け取らないため、認証ロジックが全く実行されないまま、前のユーザーのデータが新しいユーザーに表示されてしまう。
二つ目は、Varyヘッダーが不適切に設定されているパターンである。例えば、Vary: Accept-Encodingは指定されているがVary: Cookieが指定されていない場合、キャッシュは圧縮方式の違いは識別するが、ユーザーを区別するためのCookieの違いは考慮しない。結果として、あるユーザーに合わせたコンテンツが保存され、別のユーザーからのリクエストに対してもそのコンテンツが提供されることになる。これらの問題は、共有キャッシュが存在する環境、特にアプリケーション開発チームがCDNやリバースプロキシの設定を十分にレビューしていない場合に多く発生する。
このような問題を特定し、修正するためには、いくつかのポイントに注目する必要がある。まず、ユーザー固有の状態を持つレスポンス、具体的にはアカウントページ、ダッシュボード、注文履歴など、ユーザーによって内容が変化する部分を調査する。非認証のリクエストと認証済みのリクエストの両方で生の応答ヘッダーを確認し、Cache-Controlディレクティブに違いがないか、あるいは意図しないpublicやmax-ageが設定されていないかを確認する。
次に、アプリケーションの前に配置されているキャッシュ層(CDNやリバースプロキシ)自体の設定を確認することも不可欠だ。CDNのルールがファイル拡張子に基づいてキャッシュするとか、プロキシがすべてのHTTP 200応答をデフォルトでキャッシュするといった設定は、アプリケーションが意図しないキャッシュ動作を引き起こす可能性がある。最終的なキャッシュポリシーは、アプリケーションの設定とキャッシュ層の設定の双方の兼ね合いで決定されるため、両方を把握しておく必要がある。
また、ログアウト時の挙動にも注意が必要だ。ユーザーがログアウトしても、それ以前にキャッシュされた認証済みコンテンツがキャッシュから削除されない場合がある。例えば、ログアウト処理が、キャッシュ可能なページへのリダイレクトを返すPOSTリクエストで行われた場合、ログアウト前にキャッシュされたユーザー固有のコンテンツは、その有効期限が切れるまでキャッシュに残り続けてしまう。
これらの問題を解決するための具体的な対策としては、まず、すべての認証済みレスポンスに対して、明示的にCache-Control: private, no-storeを設定することが最も確実な方法である。特にno-storeは、共有キャッシュへの保存を完全に禁止する強力なディレクティブであり、ユーザーデータを含む応答には必ず含めるべきだ。
次に、もし応答がキャッシュ可能であり、かつユーザー固有の情報によって内容が変化する場合には、Vary: Cookieヘッダーを追加する。これはno-storeほど強力な制御ではないが、キャッシュキーを拡大することで、異なるユーザーに対する誤ったコンテンツ提供を防ぐことができる。共有キャッシュが避けられない環境で有効な対策となる。
さらに、CDNのキャッシュルールを、デフォルトで全てをキャッシュするのではなく、特定のパスプレフィックス(例えば/static/images/のような静的アセットのパス)に基づいて具体的に指定し、それ以外のパスは通過させるように設定することで、キャッシュポリシーの監査性を高めることができる。
そして、攻撃者の視点からシステムをテストすることが極めて重要だ。認証済みリクエストを実行した後、異なるソースから非認証のリクエストを実行し、応答のボディ内容を比較する。ヘッダーの確認だけでは意図が確認できるに過ぎず、実際にレスポンスボディを比較することで、予期せぬ情報漏洩が起こっていないかという挙動を確認できる。
最後に、認証済みパスにおけるキャッシュヒット率を監視することも有効だ。本来キャッシュされるべきではないページで高いキャッシュヒット率が観測された場合、それはインシデントに先行する運用上の兆候である可能性がある。
結論として、キャッシュを利用する際には、それぞれのキャッシュされるパスについて「キャッシュキーは何で、他に誰がそれを共有するか」という本質的な問いを常に考える必要がある。もしアプリケーションが承認のために依存するヘッダーがVaryに含まれていない場合、それは「すべてのユーザーが同じページを見る」という決定を下したことを意味する。これはマーケティングサイトのような公開情報に限定されるサイトでは妥当だが、それ以外の、ユーザー固有の情報を扱うサイトでは重大な欠陥となる。