メルカリでCDNのキャッシュに由来する情報流出があった。CDNでキャッシュしているのはリバース・プロキシで、ちょっと前にGoの練習を兼ねてリバース・プロキシを書いたので解説してみる。

リバース・プロキシのキャッシュの挙動について考えるとき、以下の2点を切り離して考える必要がある:

  • どのようなリクエスト/レスポンスのときにキャッシュするか?
  • どのようなリクエストのときにキャッシュから返すか?

メルカリのケースでは、そもそもキャッシュして欲しくない情報がキャッシュされ、かつそれがユーザへのレスポンスとして使われたという問題なので、ここでは前者の「どのようなリクエスト/レスポンスのときにキャッシュするか?」について考える。

リバース・プロキシはどのようなリクエスト/レスポンスのときにキャッシュするのか? RFC7234的には以下のすべての条件を満たすときにキャッシュするかもしれない。

  • キャッシュ可能なメソッドある
  • キャッシュ可能なステータスコードである
  • リクエストヘッダのCache-Controlno-storeがない
  • レスポンスヘッダのCache-Controlno-storeprivateがない
  • リクエストヘッダにAuthorizationがない
  • 以下のいずれかを満たす
    • レスポンスヘッダにExpiresがある、または
    • レスポンスヘッダのCache-Controlmax-ageがある、または
    • レスポンスヘッダのCache-Controls-maxageがある、または
    • レスポンスヘッダのCache-ControlにおいてCache Control Extensionsでキャッシュ可能だと指定されている、または
    • キャッシュ可能だと定義されているステータス・コード、または
    • レスポンスヘッダのCache-Controlpublicがある

メルカリのケースではレスポンス・ヘッダに以下を指定していた。

Cache-Control: no-cache
Expires: Thu, 22 Jun 2017 08:58:21 GMT (アクセスの1秒前の時間)

上に書いたキャッシュの条件で、Cache-Control: no-cacheは出てこない。RFC7234では、no-cacheはキャッシュされたレスポンスを使わないように指定するものだと書いてある。つまり、「どのようなリクエストのときにキャッシュから返すか?」の話だ。

意外なことに、古い日付のExpiresがあることはキャッシュされる原因になりうる。なぜなら、リバース・プロキシはアプリケーション・サーバ等の上流のサーバが反応しないときに、古い内容であると分かっていながらキャッシュからレスポンスを返すことがあるからだ。

では、どうすればキャッシュされなくなるか?上に書いたキャッシュの条件に当てはまらなくなればいい。具体的にはレスポンスヘッダにCache-Control: privateでよい。