理系学生日記

おまえはいつまで学生気分なのか

セキュリティ対策としての Cache-Control ヘッダについて

今日はブラウザのキャッシュ制御の話。キャッシュについては主に性能面で語られて、情報漏洩に繋がる重要な制御であることは見逃されがちです。

情報漏洩自体はよくないことで、被害にあってしまった人はそんなこと言ってられないけれど、その原因を包みかくさず公開することで他山の石というか、間違いなく日本のセキュリティ意識は向上すると思います。 ぼく自身も、みなさんも、そろそろ Cache-Control: no-cache, no-store, must-revalidate しとけば良いんやろ、というゴミのような意識を改善しなければならないということで、ここにキャッシュについてまとめてみます。

Cache の種類

ブラウザで関連するキャッシュには主に 2 つほどあります。

  1. private cache
  2. shared cache

ともに名前が示す通りですが、前者が 1 ユーザのためのキャッシュでブラウザが保持するキャッシュと思ってもらえればだいたいイメージがわくはず。 後者は忘れられがちですが、複数ユーザが共有するキャッシュで、プロキシサーバとかに保存されたり、CDN や 場合によっては LB に保存されたりして、複数ユーザに共有されます。

Cache-Control ヘッダ

HTTP 1.1 においては、このキャッシュ制御は Cache-Control ヘッダによって行われます。 Cache-Control ヘッダは、HTTP リクエストにも HTTP レスポンスにも付与可能ですが、今日のエントリはサーバ側を主軸としているので、レスポンスに付与するケースに限定して議論します。

レスポンスに付与できる Cache-Control のディレクティブとしては以下のようなものがありますが、今日は 1〜5 までをまとめてみます。

  1. no-store
  2. no-cache
  3. must-revalidate
  4. public
  5. private
  6. max-age
  7. s-maxage
  8. no-transform
  9. 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-revalidateno-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 の仕様も鑑みると 「とりあえずキャッシュさせたくなかったら、安全ぽいヤツいろいろ指定してみよ」みたいな形で指定するものだと理解するようになりました。

参考文献