クロスサイトリクエストフォージェリ(以下CSRF)とは、攻撃者があるウェブページを作り、そこに訪れた第三者に対して罠をしかけたリンクを踏ませ、知らないうちに別のサイトへ書き込みを行わせるといった攻撃法です。つまり、サイトをまたがって(クロス・サイト)、偽物(forgery)のリクエストを送るという手法です。
パソコン遠隔操作事件では、このCSRFの罠にはめられた被害者が、知らぬうちに横浜市の公式ページに書き込みを行っていたという被害が出ています。
スポンサーリンク
CSRFの対策は、送信されてきたデータが、正規のフォーム画面からのデータなのかを判定することによって行います。
今回は、セッションIDをハッシュ化したトークン(固定トークン)を利用することによる判定を行います。
仕組み
まずユーザがフォーム画面にアクセスしてきたら、サーバ側でセッションIDを元にして作成したトークンを発行します。それと同時にそのトークンをフォームのhiddenに埋め込みます。データを受け取った際にhiddenのトークンとサーバ上のトークンを比べて、もし一致していなければ、サーバ外のフォームから送信されたデータと判定できます。
1. ユーザがフォーム画面にアクセスしてきたらサーバ側でトークンを発行します。トークンはセッションIDをハッシュ関数に通してハッシュ値として生成します。
2. トークンをhiddenに埋め込みます。
3. データを受け取ったら、サーバ側のトークンを比べます。もし一致していなければ送信されたhiddenのトークンは、サーバ上のフォーム画面からの送信でないと判定できます。
実際に簡単なデモ画面を作成したのでご参照下さい。はじめのページ(csrf1.php)を開くとサーバ上でトークンが生成されます。送信ボタンをクリックすると次ページ(csrf2.php)へと遷移し、トークンが一致している様子を見ることができます。
csrf1.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?php session_start(); header("Content-type: text/html; charset=utf-8"); echo "<p>セッションIDです ".session_id()."</p>"; echo "<p>生成したトークン ".sha1(session_id())."</p>"; ?> <!DOCTYPE html> <html> <head> <title>クロスサイトリクエストフォージェリ</title> </head> <body> <p>以下で送信します。<p> <form action="csrf2.php" method="post"> 名前:<input type="text" name="yourname" value="ウェブの葉"> <input type="hidden" name="token" value="<?=sha1(session_id())?>"> <input type="submit" value="送信"> </form> </body> </html> |
7行目・21行目
セッションID( session_id() )の値を、ハッシュ関数sha1でハッシュ化しています。この値をトークン(token)としています。
csrf2.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<?php session_start(); header("Content-type: text/html; charset=utf-8"); echo "<p>前画面から受け取ったトークン ".$_POST['token']."</p>"; echo "<p>生成したトークン ".sha1(session_id())."</p>"; if(empty($_POST)) { $errors['wrong'] = "はじめの画面から進んで下さい。"; }else{ //名前入力判定 if (!isset($_POST['yourname']) || $_POST['yourname'] === "" ){ $errors['name'] = "名前が入力されていません。"; } //トークン判定 if ($_POST['token'] != sha1(session_id()) ){ $errors['token'] = "トークンが一致しません"; } } ?> <!DOCTYPE html> <html> <head> <title>クロスサイトリクエストフォージェリ</title> </head> <body> <?php if (count($errors) === 0): ?> <p>トークンが一致しました。</p> <?php elseif(count($errors) > 0): ?> <?php foreach($errors as $value){ echo "<p>".$value."</p>"; } ?> <?php endif; ?> </body> </html> |
次にcsrf1.phpのフォームの部分を以下のように変えて、ご自身のPHPの環境にてブラウザを開き、送信してみて下さい。ご自身の環境のトークンと、action先のURL上のトークンは一致しないはずです。このようにして、偽物(forgery)のリクエストかどうかを判定することができます。
csrf1_forgery.phpのフォーム部分
1 2 3 4 5 6 |
<p>以下で送信します。<p> <form action="https://noumenon-th.net/programming/sample/php/csrf2.php" method="post"> 名前:<input type="text" name="yourname" value="バカの葉"> <input type="hidden" name="token" value="<?=sha1(session_id())?>"> <input type="submit" value="送信"> </form> |
※トークンの生成方法については以下サイトもご参照下さい。
CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例