2018年11月15日木曜日

問題:CSRFの防止策に関するチートシートにツッコミを入れる

この記事は「問題:間違ったCSRF対策~中級編~」の続編です。当初上級編を意図しておりましたが、後述する事情により、級の指定は外しました。

はじめに

問題は、OWASPから出ているCross-Site Request Forgery (CSRF) Prevention Cheat Sheet(JPCERT/CCによる邦訳「クロスサイトリクエストフォージェリ (CSRF) の防止策に関するチートシート」)にツッコミを入れてもらおうというものです。具体的には、このチートシート(カンニングペーパーの意)のDouble Submit Cookie(Cookie の二重送信)の箇所です。以下、JPCERT/CC訳で該当箇所を引用します。
Cookie の二重送信
Cookie の二重送信は、Cookie およびリクエストパラメーターの双方でランダムな値を送信し、サーバー側で Cookie の値とリクエストの値が等しいかどうか検証する手法です。
ユーザーがサイトにログイン するとき、サイトは暗号強度の高い疑似ランダム値を生成し、その値を Cookie としてユーザーのマシンに、セッション ID とは別に送ります 。どんな形であれ、サイトはこの値を保存しておく必要はありません。次にサイトは、機密に関わる送信にはすべてこのランダム値が非表示のフォーム値 (または他のリクエストパラメーター) および Cookie の値として含まれていることを確認します。同一生成元ポリシーにより、攻撃者はサーバーから送信されるどんなデータも読み取ることができません。また、Cookie の値を変更することもできません。攻撃者は、任意の値を悪意のある CSRF リクエストに添付して送信できますが、Cookie に保存されている値は、変更することも、読み取ることもできません。Cookie の値と、リクエストパラメーターまたはフォームの値は同じにする必要があるので、攻撃者はランダムの CSRF 値を推測できない限り、フォームを正常に送信できません。
Direct Web Remoting (DWR) の Java ライブラリバージョン 2.0 には、CSRF 対策として、透過的に Cookieの二重送信を行う機能が組み込まれています。
要は、ログイン時に乱数でトークンを生成しておいて、それをクッキーに保存しておく。入力フォームではクッキーの値をhiddenパラメータ等に入れて、POSTパラメータとクッキーとで同じトークン値を「二重に」送信し、受け取り側で両者が一致していることを確認するというものですね。上記にもあるように、サーバー側で状態を保持する必要がなく、RESTとの相性が良いということもあり、最近人気が出始めているようです。アプリケーションフレームワークでは、Django、CodeIgniter、FuelPHP等で採用されています。

上記の説明は技術的な間違いがあるので、それを指摘してもらおう。天下のOWASPのドキュメントにいちゃもんをつけるのだから、上級問題にふさわしい…そう思い、記事執筆の準備としてチートシートの原文(英語)を久しぶりに確認しました。

…あれ?
…大幅に改定されている
…Double Submit Cookieの位置づけも変わっている

ということで、なんと「解答」が本家のサイトに掲載されているというまぬけな状況になってしまいました。改定は今年の10月に行われたようです(改訂履歴)。

まぁ、チートシートでカンニングができるという、まことにチートシートにふさわしい状況になってしまったのですが、問題自身は面白いので、初期とか上級とか抜きにして出題したいと思います。

参考実装

読者の便宜のために参考実装を以下に示します。仕様は初級編等と同じですので、画面遷移例は初級編の記事を参照してください。
以下はテスト用に「ログインしたことにする」スクリプト(mypage.php)。ログイン時にCSRF対策用のトークンを生成してクッキーにセットします。ログイン状態で呼び出した場合は、単にログインユーザのメールアドレスを表示します。(2018/11/16変更。トークンクッキーの生成タイミングを変更しました)

<?php // mypage.php ログインしたことにする確認用のスクリプト
  session_start();
  if (empty($_SESSION['id'])) {
    $_SESSION['id'] = 'alice'; // ユーザIDは alice 固定
    $_SESSION['mail'] = 'alice@example.com'; // メールアドレス初期値
  }
?><body>
ログイン中(id:<?php echo
  htmlspecialchars($_SESSION['id'], ENT_QUOTES, 'UTF-8'); ?>)<br>
メールアドレス:<?php echo
  htmlspecialchars($_SESSION['mail'], ENT_QUOTES, 'UTF-8'); ?><br>
<a href="chgmailform.php">メールアドレス変更</a><br>
</body>
以下はメールアドレス変更フォーム(chgmailform.php)です。クッキーからトークンを取り出してhiddenパラメータにセットしています。クッキーにトークンがあればそのまま使い、なければ新規に生成しています。
<?php // chgmailform.php メールアドレス変更フォーム
  session_start();
  if (empty($_SESSION['id'])) {
    die('ログインしてください');
  }
  $token = filter_input(INPUT_COOKIE, 'token');
  if (empty($token)) {
    $token = bin2hex(random_bytes(24));  // CSRFトークンの生成
    setcookie('token', $token);          // CSRFトークンをクッキーに保存
  }
?><body>
<form action="chgmail.php" method="POST">
メールアドレス<input name="mail"><BR>
<input type=submit value="メールアドレス変更">
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token, ENT_COMPAT, 'UTF-8'); ?>">
</form>
</body>
以下はメールアドレス変更プログラム(chgmail.php)。POSTパラメータとクッキーのトークンを確認の後、メールアドレスを変更(実際にはセッション変数のみ変更)します。
<?php // chgmail.php メールアドレス変更実行
  session_start();
  if (empty($_SESSION['id'])) {
    die('ログインしてください');
  }
  $id = $_SESSION['id'];
  $c_token = filter_input(INPUT_COOKIE, 'token');
  $p_token = filter_input(INPUT_POST, 'token');
  if (! hash_equals($c_token, $p_token)) {
    die('正規の画面からご使用ください');
  }
  $mail = filter_input(INPUT_POST, 'mail');
  $_SESSION['mail'] = $mail;  // メールアドレスの変更
?>
<body>
<?php echo htmlspecialchars($id, ENT_COMPAT, 'UTF-8'); ?>さんのメールアドレスを<?php
 echo htmlspecialchars($mail, ENT_COMPAT, 'UTF-8'); ?>に変更しました<br>
<a href="mypage.php">マイページ</a>
</body>
メールアドレス変更時のPOSTリクエスト例を以下に示します。
POST http://example.jp/chgmail.php HTTP/1.1
Host: example.jp
Content-Length: 79
Cache-Control: max-age=0
Origin: http://example.jp
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://example.jp/chgmailform.php
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=onvi06g301m1o2cb1i0lgm9neg; token=fe24151956678c544bef14258407e12032ada4216f36480b
Connection: close

mail=alice%40wasbook.org&token=fe24151956678c544bef14258407e12032ada4216f36480b
クッキーとPOSTパラメータで、同じトークンが二重に送信されていることがわかります。

(2018/11/16追記)
Herokuにデモ環境を用意しました
https://csrf-vul.herokuapp.com/mypage.php

設問

チートシート旧版の翻訳であるJPCERT/CC訳(前述の引用部分)を元に以下の設問に答えよ。
  • 引用部分の解説には技術的な間違いがある。それを指摘せよ
  • クッキーの二重送信でCSRF保護できないシナリオを複数指摘せよ。OWASP原文の改定で指摘されていないシナリオを指摘すると加点となる

解答は週明けに公開予定です。

0 件のコメント:

コメントを投稿

フォロワー

ブログ アーカイブ