やられアプリ BadTodo - 28 キャッシュからの情報漏洩

前回:やられアプリ BadTodo - 26 レースコンディション - demandosigno

キャッシュを利用することでアプリケーションの読み込み処理を高速化したり、サーバーの負荷を軽減させたりできます。
BadTodoでは Nginx がリバースプロキシサーバとなりキャッシュ機能を持っています。

今、"test"ユーザでBadTodoにログインしマイページを見ています。(これ以降少し時間のかかる操作のため、ログイン時に「ログインしたままにする」をチェックしておきます)

このときURLに"rnd=6603fd696be28"という文字列が付いています。これは「キャッシュバスター」といい、URLのクエリー文字列として乱数値を付与することでキャッシュからの情報漏洩を防ぐ保険的な対策の一つです。

ここでは、キャッシュバスターが付いていない場合について見ていきます。
アドレスから"rnd=6603fd696be28"を削除してアクセスします。

次にブラウザをリロードし、同じURLをもう一度読み込んでみます。(「マイページ」の再クリックでは"rnd"が付くのでダメです)
このとき、レスポンスに"X-Cache: HIT"と出ており、2回目のアクセスはキャッシュから読み込まれたことが分かります。(サーバにキャッシュが保存されている)

次に、今アクセスしているブラウザ(BurpSuite組み込みChromium)とは別のブラウザを一つ立ち上げます。(今回はFirefoxで試しています)
サーバーのキャッシュだけではなくブラウザにもキャッシュ機能はありますので、まず最初にブラウザ設定ページからブラウザ側のキャッシュは削除しておいてください。(「設定」→「プライバシーとセキュリティ」→「Cookieとサイトデータ」「データを消去」)
そして、先ほどのプロフィールページのアドレスをコピーして二つ目のブラウザでアクセスします。

まだログインもしていないのにも関わらず、"test"ユーザのプロフィールが見えてしまいました。

Firefoxの方もBurpを通して見てみると"X-Cache: HIT"となっており、キャッシュされたマイページを見ていることになります。

Nginxのデフォルトでは応答がキャッシュされる時間は無制限です。指定のログファイルサイズを超えれば削除されますが、そうでなければ永遠に残ります。
BadTodoの場合、設定ファイルにて"proxy_cache_valid 200 302 180s;"と指定されているため、200, 302のステータス応答に対しては180秒有効となっています。
(設定ファイルの場所はソースフォルダ上では \badtodo\nginx\default.conf に。badtodo-eginxコンテナ上では /etc/nginx/conf.d/default.conf 内で記述されています)

実際3分を超えた時点で"X-Cache: EXPIRED"となりキャッシュ切れとなります。

ここで、そのままFirefoxのページをリロードすると、今度は"X-Cache: HIT"となります。

続けて、元のChromiumブラウザ側をリロードするとログアウトされてしまったように見えます。

しかし、これは先ほどのFirefox側のログアウト画面をキャッシュしてしまったことによる「なんちゃってログアウト」画面です。
同様に3分待ってからリロードすると同じセッションIDのままログイン状態が継続していることを確認できます。

対策

  • アプリケーション側でキャッシュ制御用の適切なレスポンスヘッダを設定する
  • キャッシュサーバ―側でキャッシュ制御の適切な設定を行う

アプリケーション側でキャッシュを抑制するには、Cache-Controlヘッダとして no-store を指定すればよいですが、ブラウザやキャッシュサーバの仕様のブレを考慮して以下を指定すると良いでしょう。
Cache-Control: private, no-store, no-cache, must-revalidate
Pragma: no-cache

PHPの場合、session_cache_limiter関数を使い session_cache_limiter('nocache'); と指定することで
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
のようなヘッダが出力されます。
PHP: session_cache_limiter - Manual

ただ、session_cache_limiter() は session_start() がコールされる必要があり、BadTodoは独自のセッション生成方法を使っているためか機能しませんでした。 今回は一旦 common.php の先頭に直接記述することにしました。

<?php
header("Cache-Control: private, no-store, no-cache, must-revalidate");
header("Pragma:no-cache");

これにより所定のヘッダが付与されます。

Firefox側で読み込んでも"X-Cache: MISS"となります。アプリケーション側での拒否指示が優先されます(Nginx側でキャッシュされません)。

サーバー側の設定については後日。

補足:確認の際に思うように動作しなくなったら一旦キャッシュを削除してみてください。
BadTodo - Nginxのキャッシュの削除

Cache-Control - HTTP | MDN
www.itmedia.co.jp

/* -----codeの行番号----- */