CSRF対策として、hiddenパラメータにセッションIDを出力する手法がある。この手法を採用するとき、HTTPレスポンスのキャッシュはどう制御すべきだろうか。

非共有キャッシュは活用したい

まず、一切キャッシュさせない方針がありうる(Cache-Control: no-store)。これで問題ないのであれば、もう何も考える必要はない。今回の記事では、そうではなく、非共有キャッシュを活用する方針を前提としたい(Cache-Control: private)。REST厨だからだ。

ここから先は、キャッシュの有効期限を明示的に指定できるかどうかによって話が変わってくる。

有効期限を明示的に指定できる場合

有効期限を明示的に指定できる場合は、以下のようなレスポンスヘッダを返すのがよいだろう。

Cache-Control: private,max-age=1800
Vary: Cookie

Cache-Controlで、非共有キャッシュへのキャッシュを許可するとともに、有効期限を明示的に指定する。

重要なのは「Vary: Cookie」だ。指定しないと、再ログインによってセッションIDが変わったり、ログアウトによってセッションIDが無効になったりしても、有効期限内であればキャッシュが使われてしまう。

有効期限を明示的に指定できない場合

有効期限を明示的に指定できない場合は、以下のようなレスポンスヘッダを返すのがよいだろう。

Cache-Control: private,must-revalidate
ETag: "hogehoge"
Last-Modified: Thu, 13 Sep 2012 01:23:45 GMT

有効期限を明示的に指定できないのは、常に新鮮なレスポンスを返したいからだ。よって「Cache-Control: must-revalidate」を指定し、キャッシュの常時検証をクライアントに指示するのがよいだろう。

検証条件として、上記の例ではETagとLast-Modifiedを返している。少なくとも一方は返し、If-None-MatchやIf-Modified-Sinceを用いた条件付きGETに対応する必要がある。

「Vary: Cookie」がないのは、セッションIDの変化に応じてETagやLast-Modifiedが変わるのを前提としているためだ。気になるなら返してもよいだろう。

JavaScriptでキャッシュをさらに有効に使う

さらに考えると、セッションIDが変わっただけでキャッシュが無効になるのは、もったいない気がしてくる。hiddenパラメータへのセッションIDのセットをクライアントサイドで処理するのはどうだろうか。文脈は違うが「Kazuho@Cybozu Labs: CSRF 対策 w. JavaScript」や「畑@サイボウズ・ラボ - CSRF対策 with JavaScript」で紹介されている手法である。

メリットとして、レスポンスにプライベートな情報が含まれない場合に、共有キャッシュの使用が可能になる(Cache-Control: public)。

また、セッションIDが変わっただけではキャッシュの検証が不要になる。キャッシュのヒット率が上がるし、サーバ側はリソースの本質的な変化だけを気にすればよいから実装が楽になる。良いことずくめではないだろうか。