今日はブラウザのキャッシュ制御の話。キャッシュについては主に性能面で語られて、情報漏洩に繋がる重要な制御であることは見逃されがちです。
情報漏洩自体はよくないことで、被害にあってしまった人はそんなこと言ってられないけれど、その原因を包みかくさず公開することで他山の石というか、間違いなく日本のセキュリティ意識は向上すると思います。
ぼく自身も、みなさんも、そろそろ Cache-Control: no-cache, no-store, must-revalidate
しとけば良いんやろ、というゴミのような意識を改善しなければならないということで、ここにキャッシュについてまとめてみます。
Cache の種類
ブラウザで関連するキャッシュには主に 2 つほどあります。
- private cache
- shared cache
ともに名前が示す通りですが、前者が 1 ユーザのためのキャッシュでブラウザが保持するキャッシュと思ってもらえればだいたいイメージがわくはず。 後者は忘れられがちですが、複数ユーザが共有するキャッシュで、プロキシサーバとかに保存されたり、CDN や 場合によっては LB に保存されたりして、複数ユーザに共有されます。
Cache-Control ヘッダ
HTTP 1.1 においては、このキャッシュ制御は Cache-Control
ヘッダによって行われます。
Cache-Control ヘッダは、HTTP リクエストにも HTTP レスポンスにも付与可能ですが、今日のエントリはサーバ側を主軸としているので、レスポンスに付与するケースに限定して議論します。
レスポンスに付与できる Cache-Control のディレクティブとしては以下のようなものがありますが、今日は 1〜5 までをまとめてみます。
no-store
no-cache
must-revalidate
public
private
max-age
s-maxage
no-transform
proxy-revalidate
no-store
もっとも直感的に分かりやすいディレクティブで、private cache に対しても shared cache に対しても、「キャッシュするな(MUST NOT)」という指示になります。
余談ですが、メルカリのエンジニアブログによると、メルカリの使っていた CDN は、このディレクティブが使われている場合であってもキャッシュをするような振舞いに見えます。no-store
過信できんということか。
キャッシュをしないのは
Cache-Control: private
が含まれている場合のみ CDN切り替え作業における、Web版メルカリの個人情報流出の原因につきまして | メルカリエンジニアリング
no-cache
メルカリが使用していたという no-cache
ですが、これを"キャッシュさせない"という制御に使うのは悪手だったのかなと思っています。
ディレクティブがクソ分かりづらいですが、これは「キャッシュするな」ではなく、キャッシュしても良いけどそれを使うときはちゃんと "検証" してね、というものだからです。
IPA から引用します。
一見「キャッシュを使うな」のように見えるこのヘッダが実際に意味するところは少々ニュアンスが異なる。このヘッダの意味は、いちどキャッシュに記録されたコンテンツは、現在でも有効か否かを本来のWebサーバに問い合わせて確認がとれない限り再利用してはならない、という意味である。 IPA ISEC セキュア・プログラミング講座:Webアプリケーション編 第5章 暴露対策:プロキシキャッシュ対策
ここでの "検証" (Validation) っていうのは、RFC7232 の Conditional Requests ってヤツです。もうちょっというと If-Modified-Since
とか ETag
ヘッダとかが馴染み深いと思います。
no-cache
ディレクティブは、これらのヘッダを使って、サーバ側に「自分が持っているキャッシュが最新か否か」を問い合わせることを強制するってことですね。
must-revalidate
must-revalidate
は no-cache
とよく混同されますし、事実、かなり分かりづらいです。ぼく自身もこの違いってよく分かっていません。
古くなったキャッシュは origin server に検証することなく使ってはならない、というディレクティブだと解釈しているんですが、合ってるかな。
Cache-Controlヘッダは仕様通り実装されていない?(2) - Qiita
の記事の検証結果を見る限り、合ってると思うんだけどな。
以下の記事も面白いので、興味がある方はご覧ください。
public
public
はわりと危険なヘッダです。
「キャッシュしても良いよ」という意味ではあるのですが、このディレクティブを付与すると、通常はキャッシュされない Authorization
ヘッダ付きのレスポンスだってキャッシュしちゃいます。
(このあたりは、must-revalidate
とか、s-maxage
とかも関係するけど、詳細は RFC7234 を見てくれ)
private
メルカリのエンジニアブログに記載のある CDN は、このディレクティブがあるときだけキャッシュしないという仕様なようですが、
private
ディレクティブは「shared cache にはキャッシュするな」を示すものです。
Cache-Control: private, no-store, no-cache, must-revalidate
今回のエントリ記述のモチベーションは、Cache-Control: private, no-store, no-cache, must-revalidate
っていう決まり文句みたいなのはどういう意味を持っているんだろう、というとこからはじまりました (しばらくグダグダしてたら、メルカリの漏洩があって、ようやくまとめた)。
最初は、「それぞれのディレクティブが別のことを指定しており、総体としてセキュアな指定になっている」と思っていたんですが、メルカリの使っている CDN の仕様も鑑みると
「とりあえずキャッシュさせたくなかったら、安全ぽいヤツいろいろ指定してみよ」みたいな形で指定するものだと理解するようになりました。
参考文献
- RFC 7234 - Hypertext Transfer Protocol (HTTP/1.1): Caching
- IPA ISEC セキュア・プログラミング講座:Webアプリケーション編 第5章 暴露対策:プロキシキャッシュ対策
- CDN切り替え作業における、Web版メルカリの個人情報流出の原因につきまして | メルカリエンジニアリング
- RFC 7232 - Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests
- Cache-Controlヘッダは仕様通り実装されていない?(2) - Qiita
- http - Difference between no-cache and must-revalidate - Stack Overflow
- Cache-Control - HTTP | MDN