Contents
キャッシュを防ぐ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-storeにmax-age=0が暗黙的に含まれることは無い”旨を「Web配信の技術」の著者様からコメントいただいた為訂正しています。詳細はコメントを参照してください。
no-cache
- コンテンツをどのキャッシュにも格納することができるが、キャッシュを検証無しで使ってはならない。
- must-revalidateは有効期限切れのキャッシュの場合に再検証するが、no-cacheは毎回コンテンツが最新か検証する。その為、no-cacheとmax-ageは共存できない。
must-revalidate
- 有効期限切れのキャッシュを利用する場合は必ずオリジンに問い合わせを行う。
- オリジンサーバのダウンなどで再検証ができなかった場合は504(Gateway Timeout)を返す。
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レスポンスに含めると、相手のプロキシが対応してくれる場合もある。
プロキシキャッシュ対策 | IPA
Pragma: no-cache
キャッシュを防ぐには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で期限切れキャッシュを利用する際にオリジンサーバへの問い合わせを強制することができます。
初めましてWeb配信の技術の著者です。
非常に読み込んでいただけいて非常にうれしいです。
ただ、1つ気になる点がありました。
> max-age=0が暗黙で含まれるため、must-revalidateは意味を持たなくなる。
こちらですが、おそらくMDNのno-storeの記述からだとおもうのですが
no-storeにはmax-age=0が暗黙的に含まれていることはありません。
日本語版のMDNではno-storeはキャッシュを保存しないわけだから実質的にmax-age=0が含まれているといった意味合いだと思うのですが、
そもそもこれは2重の意味で間違えています。
– max-age=0はキャッシュされないわけではないのでmust-revalidateが意味を持たなくなるわけではない(本でも触れておりますが、条件によっては再検証なしでstaleキャッシュが再利用されます)
– そもそもno-storeにはmax-age=0が暗黙で含まれていない
また、日本語版のMDNではそのような記載はあるのですが、原文では既に修正されております。
https://github.com/mdn/content/pull/115/files#diff-00cc5178fdf5a5e02de7501c3dd67fa85d806dd66861165a0523b63954aef950
(ちなみにキャッシュ関連のドキュメントは、別のPRでさらに改善がされております。 https://github.com/mdn/content/pull/10027 )
MDNの当初の間違いはおそらくレスポンスのみを考えており、Cache-Controlがリクエスト中に含まれることがあることを想定していなかったのではないかと思います。(ブラウザでリロードすると発行されるのが見えます)
この間違いを理解するには2つのポイントを知っている必要があります。
1. キャッシュは「行う」時と「使う」時の2つのステージがあること
2. Cache-Controlはクライアントからのリクエスト時にも使われることがある
no-storeディレクティブは本でも解説しておりますが、キャッシュを「行う」時に評価されるものです。
RFCのNote( https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.1.5 )にも記載はあるのですが、既にキャッシュされているコンテンツがある場合、このディレクティブは影響を及ぼさないということが書かれています。(行う時のみの評価のため)
もしmax-age=0が暗黙的に含まれるのであれば、リクエスト時にCache-Control: no-storeを指定したとしてもキャッシュが使われない指定となるはずですがそうはなりません(Chromeで確認しております)
以上のことより暗黙的に含まれていることはないかなと思います。
コメントありがとうございます!
まさか著者の方に指摘をいただけるとは思っておらず、、大変驚きました。
内容についてですが、おっしゃる通りMDN Web Docsを参考に記載したものでした。
原文を確認しましたが、確かにno-storeでmax-age云々は記載されていませんね。
詳細にご教示いただきありがとうございました。
ITエンジニアとして、原文を読む大切さを改めて教えられた気分です。