2015年3月27日金曜日

キャッシュ制御不備の脆弱性にご用心

古い書籍に掲載されたPHP記述の掲示板ソフトを動かしていると、ログアウト処理がうまく動作していないことに気がつきました。チェックの方法ですが、ログアウト処理の脆弱性検査の簡単なものは、「安全なウェブサイトの作り方」別冊の「ウェブ健康診断仕様」に記載されています。
(J)認証
ログアウト機能はあるか、適切に実装されているか
ログアウト機能がない、あるいはログアウト後「戻る」ボタンでセッションを再開できる場合
この仕様書にある通り、『ログアウト後「戻る」ボタンでセッションを再開できる』状態でした。
おそらくセッション破棄がきちんと書かれていないのだろうと思いログアウト部分のソースを見ると、以下の様な処理内容でした(オリジナルからはリライトしています)。
<?php // logout.php
  require_once('common.php'); // 共通の設定・処理
  session_start();   // セッションの開始
  session_destroy(); // セッションの破棄
  header('Location: http://example.jp/');
  exit();
ソース上では、きちんとセッションが破棄されているように見えます。そこでさらに動作を確認していくと、以下が分かりました。
  • 1回目のログアウト処理はきちんと動作するが、2回目以降のログアウト処理は表示上は正しい遷移だが、セッションは破棄されていない
  • logout.phpは、ボタンではなくリンクで呼ばれている。すなわちGETメソッドで呼ばれている
2回目以降がおかしいということで、キャッシュが原因だろうということでHTTPレスポンスを確認すると、以下のヘッダが付与されています。
Cache-Control:private, max-age=10800, pre-check=10800
このヘッダは共通処理が書かれた common.php 内の以下の記述によるものでした。
session_cache_limiter('private');   // 戻るボタンを有効にする
Cache-Control: privateは、個人用のキャッシュは許すが、他人にそのキャッシュは使わせないという設定で、典型的にはブラウザのキャッシュは許すが、プロキシサーバーのキャッシュは許さないという設定です。
すなわち、logout.phpに対するリクエストは、ブラウザのキャッシュは有効であることから、2回目以降はリクエストされず、キャッシュされたレスポンスが用いられていたことになります。これが「2回目以降は見かけ上はログアウトの遷移をたどるがログアウトされていない」原因です。

対策

ログアウトのように副作用を伴う処理は、キャッシュが有効では処理が行われない場合があるので、キャッシュを無効にする必要があります。PHPの場合、以下により記述できます。ただし、PHPはデフォルトでこの設定なので、通常は明示的にこれを指定する必要はありません。
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
また、副作用を伴う処理に対するリクエストにはPOSTメソッドを用いるべきです。ログアウト処理の場合、POSTではなくGETメソッドで呼び出している例をよく見かけますが、本来GETメソッドは副作用のないリクエストに用いるもので、副作用があるリクエストはPOST(あるいはPUT、DELETE)を用いることになっています。POSTメソッドの場合、絶対にキャッシュされないわけではありませんが、GETの場合に比べてキャッシュされにくいため、キャッシュ制御に関しても安全方向に働きます。

※注記
POSTリクエストがキャッシュされる例については、こちらを参照。また、ジャパンネット銀行のバンキングサイトは全ページPOSTメソッドという珍しい作り(セッションIDをPOSTで送っている)ですが、戻るボタンが使え、その場合はPOSTに対するキャッシュが表示されるようです。

キャッシュからの情報漏洩に注意

次に説明するのはキャッシュからの情報漏洩です。こちらの方が、ありがちな問題だと思います。
キャッシュ制御が不十分な場合、プロキシサーバー等にキャッシュされた秘密情報が、別人のブラウザに表示される場合があります。
以下は、キャッシュ制御脆弱な例です。現在のログインIDを表示するページの実装例です。
<?php  // profile.php
  session_cache_limiter('public'); // キャッシュ制御をpublicに
  session_start();
  $id = $_SESSION['id'];  // ログインIDを取り出し
?><body>
id: <?php echo htmlspecialchars($id); // ログインIDを表示 ?>
</body> 
session_cache_limiter('public'); により、このHTTPメッセージはプロキシサーバーによりキャッシュされます。同じプロキシサーバー(実験ではUbuntu12.04上のsquid3を使用)に接続しているパソコン2台を用い、1台目のパソコンでwatanabeでログインし、2台目のパソコンで未ログインの状態で profile.phpを表示したところ、watanabeが表示されました。実験に使用したブラウザは2台ともIE11です。

キャッシュ制御不備による影響

キャッシュ制御不備により、同じプロキシサーバーの利用者が別人の個人情報を閲覧する(いわゆる別人問題)可能性があります。さらに、プロキシサーバーの仕様や設定によっては、Set-Cookieレスポンスヘッダがキャッシュ、共有されてしまい、セッションIDの共有によるセッションハイジャックが起こる可能性があります。
Set-Cookieヘッダのキャッシュに関しては、こちらこちらに言及があります。
プロキシー・サーバーが応答を受け取ったときにキャッシュに入れない set-cookie ヘッダーを指定します。デフォルトでは、プロキシー・サーバーは set-cookie ヘッダーをプロキシー・キャッシュに保管します。Cache-Control ヘッダー情報が正しく設定されていない場合、プロキシー・サーバーは、セッションに関連したユーザー・プライベート Cookie を保管する場合があります。
http://www-01.ibm.com/support/knowledgecenter/SSAW57_8.5.5/com.ibm.websphere.nd.doc/ae/rjpx_siphttpcustprops.html?lang=ja より引用
上記は、IBM製品のドキュメントからの引用ですが、恐ろしいことが書いてありますね。これによるセッションIDの共有等が起きないように、適切にキャッシュ制御とプロキシサーバーの設定を行う必要があります。

対策

レスポンスヘッダによるキャッシュ無効化については、前述のとおりです。参照系ページの場合POSTメソッドは使えないので、キャッシュ制御をもれなく行うことで対策となります。また、リバースプロキシ(キャッシュサーバー)を使っている場合は、キャッシュのON/OFFが適切に実施されていることや、Set-Cookieヘッダのキャッシュが無効化されていることを確認してください。
キャッシュ制御の仕様を明確にするために、画面遷移図等にキャッシュ可否の印をつけるようにするとよいと思います。

まとめ

キャッシュ制御不備の脆弱性について紹介しました。後半に説明したキャッシュサーバーによる別人問題はわりあいよく知られた内容ですが、冒頭で説明した「ブラウザキャッシュによりログアウト処理が無効になる」という例は私自身初めて見たもので、珍しい例ではないかと思います。
どちらの例でも、キャッシュ制御を適切に行う、すなわち適切なレスポンスヘッダを送信することで対策になります。ただし、性能上の理由から、できるだけ多くのページをキャッシュさせたいというニーズもあるでしょうから、キャッシュの可否を仕様として明確にしておき、仕様通りのキャッシュ制御ができているかを確認するとよいでしょう。

0 件のコメント:

コメントを投稿

フォロワー