キャッシュされたくない場合のHTTPヘッダのベストプラクティス

キャッシュを防ぐHTTPヘッダの鉄板

他人のアカウント情報がキャッシュからレスポンスされてしまう(いわゆる別人問題)といった事故を防ぐために、HTTPヘッダでキャッシュを制御する、というものがセキュリティのお話では頻出します。

そして、その場合の鉄板的なベストプラクティスが、以下のHTTPヘッダを追加することです。

Cache-Control: private, no-store, no-cache, must-revalidate
Pragma: no-cache

本記事では、キャッシュを防ぐためにはなぜ上記のHTTPヘッダが必要なのかについて解説します。

各ディレクティブについて

本文に入る前に、Cache-Controlのディレクティブについて簡単な説明を載せておきます。

private

  • ローカルへのキャッシュの格納のみを許可する。
  • また、通常キャッシュできないパターンもキャッシュされる(例えば403)。
  • privateが未指定の場合は、ほとんどpublicと同じ動きになるため、経路上(CDN/Proxy)にもキャッシュが格納される。

no-store

  • コンテンツをキャッシュに格納しない。
  • max-age=0が暗黙で含まれるため、must-revalidateは意味を持たなくなる。

no-cache

  • コンテンツをどのキャッシュにも格納することができるが、キャッシュを検証無しで使ってはならない。
  • must-revalidateは有効期限切れのキャッシュの場合に再検証するが、no-cacheは毎回コンテンツが最新か検証する。その為、no-cacheとmax-ageは共存できない。

must-revalidate

  • 有効期限切れのキャッシュを利用する場合は必ずオリジンに問い合わせを行う。
  • オリジンサーバのダウンなどで再検証ができなかった場合は504(Gateway Timeout)を返す。
Pragma: no-cache について

Pragmaは、HTTP/1.1を理解できない、HTTP/1.0 クライアントとの下位互換性の為の設定です。「Pragma: no-cache」と「Cache-Control: no-cache」は意味合い的には同じです。

Expiresヘッダについて

また、キャッシュを防ぐ方法として、Cache-ControlとPragmaに加えて、Expiresヘッダを以下のように定義している場合があります。

Expires: -1

ここではExpiresの値として-1を設定していますが、本来Expiresの値として有効な形式は以下のようなグリニッジ標準時です。

Expires: Wed, 21 Oct 2015 07:28:00 GMT

Expiresヘッダは-1や0のような無効な日付は過去の日付とし、期限切れであることを意味します。

ただし、下記のようにほとんどの環境でmax-ageが使えるようなので、max-age=0を暗黙に含んだno-storeを定義していれば、あえてExpiresまで定義する必要性は薄いかもしれません。

Cache-Controlヘッダのmax-ageでは相対的にキャッシュ時間を指定します、対して、Expiresヘッダは値に時刻を入れることで、期限切れを絶対時間で指定します。

ExpiresとCache-Control:max-ageは用途が近いですが、どちらを使えば良いのでしょうか?基本的にはCache-Control:max-ageを優先すべきです。

現状、そもそもExpiresはCache-Controlを解さないクライアント向けのものです。max-ageと同時に指定された場合はmax-ageが優先されます。

互換性のために登場の古いExpiresを使うという判断もなくはありませんが、ほとんどのブラウザやProxy/CDNでmax-ageが使えます。

Expiresを使うとしてもCache-Control:max-ageとの併用がいいでしょう。

Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する

有名どころの本やIPAのキャッシュの防ぎ方

キャッシュを防ぐ方法について、技術書やIPAのサイトではどのようなディレクティブを紹介しているのかを確認してみました。

一般的にキャッシュで事故が起こる背景にあるのは、経路上でキャッシュが行われること、それが本来見えるべきでないクライアントに見えてしまうことです。

事故を防ぐには、まずprivateの設定で経路上のキャッシュを避けることが必要です。キャッシュへ保存させないために、no-storeの設定が必要です。キャッシュを使わないno-cacheも指定します。最後にオリジンがダウンしていた場合にキャッシュが使われることを防ぐため、must-revalidateを指定します。

no-store以外にもいくつか指定を加えているのは、ProxyやCDN間の互換性の問題をなるべく軽減するためです。

これらを踏まえると、次のような設定になります。

Cache-Control: private, no-store, no-cache, must-revalidate

(RFC7234の改定に関して)なお「キャッシュを使う条件」についてはPragma:no-cacheの条件が消えました。これはCache-Controlが広く普及したので、古いPragmaを非推奨としたためです。

Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する

アプリケーション側でキャッシュを抑制するためには、前述のようにCache-Controlヘッダとしてno-storeを指定すればよいことになりますが、ブラウザやキャッシュサーバーの仕様のブレを考慮して以下を指定するとよいでしょう。

Cache-Control: private, no-store, no-cache, must-revalidate

Pragma: no-cache

Pragma: no-cacheは、HTTP/1.1に非対応の古いソフトウェアのための伝統的な記法ですが、規格化されたものではなく、絶対に安全ということではありません。

体系的に学ぶ 安全なWebアプリケーションの作り方 第2版 脆弱性が生まれる原理と対策の実践

これらのヘッダを適宜取捨選択して使用することになるが、これらをすべて指定して次のようにしても構わない。

Cache-Control: private, no-store, no-cache, must-revalidate

場合によっては、古い設備が存在し、HTTP/1.1 の Cache-Control ヘッダを解さないプロキシが存在するかもしれない。このような心配のある場合は次のヘッダをHTTPレスポンスに含めると、相手のプロキシが対応してくれる場合もある。

Pragma: no-cache

プロキシキャッシュ対策 | IPA

キャッシュを防ぐにはno-storeだけでも問題ないのか

このように、各ソースのニュアンスはどれも、基本的にはno-storeだけでもよいが、ProxyやCDNによってディレクティブの解釈によってブレがあるのでこれら全てを指定することを推奨する、といったものです。

また、MDN Web Docsのno-storeの説明には以下のような文が記載されています。

他のディレクティブを設定することもできますが、最近のブラウザーではコンテンツがキャッシュされることを防ぐために必要なディレクティブはこれだけです。

Cache-Control | MDN Web Docs

上記から言うと、最低限no-storeがあれば、キャッシュに関するセキュリティ対策は考慮されていると言ってもいいかもしれません。ただし、”最近のブラウザーでは”とあるように、経路上のProxy/CDNのキャッシュに関して言及しているわけではない点に注意してください。

また、Web配信の技術には以下の文が記載されています。

特に事故を起こしやすいのがCache-Controlです。キャッシュさせないつもりがキャッシュされていたという事故はよく見かけます。たとえば、CDNによってはno-cache/no-storeを見ないところもあります(キャッシュ回避にはprivate指定が必要)。

Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する

このように、キャッシュを防ぐ方法としてのno-storeの指定は絶対的なものではないので、万全を期すのであれば、Cache-Controlのprivate/no-cache/no-store/must-revalidateの4つの指定はやはり必要です。

また、Pragmaの指定は今後非推奨になっていくと思われますが、まだお守りとしての効果はありそうです。

Cache-Controlのディレクティブが競合している場合

最後に、Cache-Controlの競合についてまとめます。

そもそも、private/no-cache/no-store/must-revalidateを指定した場合に最も優先されるディテクティブは何でしょうか。

感覚的に、最も制限が厳しいno-storeだと考えるはずですが、実際のところの優先順位が解説されているドキュメントをあまり見たことがありません。

Web配信の技術では、”RFC7234の改定で、ディレクティブが競合している際の取り扱いについて明瞭になった”旨が記載されています。つまり、今までは曖昧な部分もあったようです。

(RFC7234の改定に関して)ディレクティブが重複している、競合している際の取り扱いについても明瞭になっています。

Expiresやmax-ageの定義がそれぞれ複数ある場合(重複定義)は最初に出現したものを利用するか、応答自体をStaleとみなします。

max-ageとno-cacheが同時に定義されている場合(競合定義)は、最も制限されている定義が優先されます(例ならno-cache)。

Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する

「最も制限されている定義が優先されます」より、基本的にはno-storeが優先されることが分かります。

もし経路上のProxy/CDNがno-storeを理解できなくても、privateを理解できれば経路上にキャッシュされることは避けられます。

また、privateだけだと期限切れのブラウザキャッシュを利用する可能性がありますが、no-cacheはキャッシュ利用前に必ず検証が必要なので、キャッシュを未検証で利用することはありません。

万が一no-cacheも理解されなかったとしても、must-revalidateで期限切れキャッシュを利用する際にオリジンサーバへの問い合わせを強制することができます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)