はじめに
Web アプリケーションのログアウト処理で、CSRF (クロスサイトリクエストフォージェリ)の対策をしているものは意外と少ないのではないかと思います。徳丸先生も以下のようにおっしゃっていますし。
ログアウト処理にCSRF対策していないサイトは多いので、ログアウト処理でもきっちりトークン入れとけば「こいつは堅そうだ」と攻撃者が避けて通る…という期待は甘いよね ^^;
— 徳丸 浩 (@ockeghem)
ログアウト処理で CSRF 対策を行うことは必要なのか、もしくは必要ないのかを考察し、ログアウトのCSRF 対策の具体例を提示するのがこの記事の目的です。
CSRF とはどのような脆弱性なのか
CSRF と言われてどのような脆弱性かピンとこない方もいるかと思うので、ここで CSRF の脆弱性の定義を見直してみましょう。Web アプリケーション開発者には定番の「安全なウェブサイトの作り方」から定義を引用します。
ログ インした利用者からのリクエストについて、その利用者が意図したリクエストであるかどうかを識別する仕 組みを持たないウェブサイトは、外部サイトを経由した悪意のあるリクエストを受け入れてしまう場合があ ります。このようなウェブサイトにログインした利用者は、悪意のある人が用意した罠により、利用者が予 期しない処理を実行させられてしまう可能性があります。このような問題を「CSRF(Cross-Site Request Forgeries/クロスサイト・リクエスト・フォージェリ)の脆弱性」と呼び、これを悪用した攻撃を、「CSRF 攻 撃」と呼びます。
—「安全なウェブサイトの作り方」CSRF(クロスサイト・リクエスト・フォージェリ)より
要は、ユーザーがログインした状態で他のサイトにアクセスし、罠のリンクを踏んだら、ログインしていたサイトで意図しないコマンドが実行されてしまうということです。
やっかいなのが、ユーザーがログインした状態でこの攻撃は行われてしまうので、CSRF 対策をしていないと、本物のユーザーのリクエストなのか攻撃者のリクエストなのか判断がつかないことです。また、CSRF 対策は、Web アプリケーションの設計時から対策を行わないと、後で対策しようとしても非常に影響範囲が大きくなってしまいます。
CSRF 対策にはいくつかの方法がありますが、メジャーなのはログイン時にトークン(ランダムな文字列)を発行する方法でしょう。この場合、トークンをセッションに格納後、各ページにトークンを出力し、各ページからリクエストが来たとき、セッションに格納してあるトークンと各ページのトークンを比較し、一致しない場合は不正なリクエストと判断します。
CSRF はユーザーがサイトの管理者だったりすると、影響が大きくなるので確実に対策しておきたい脆弱性になりますね。
ログアウトの CSRF 対策の必要性について考察
CSRF の脆弱性で問題になるのは、ユーザーが意図しないコマンドを実行されて Web アプリケーションで被害が出ることになります。例えば、ユーザーアカウトの削除などが好例です。
では、ログアウト処理に CSRF の脆弱性があった場合、Web アプリケーションで被害がでるでしょうか。
意外なことに、ユーザーはログアウトされるだけで実質的な被害は何もないのです。まぁ、ID とパスワードを忘れたユーザーから問い合わせくらいは発生するかもしれませんが、それはユーザー側の責任ですし。
ですので、CSRF の対策でログアウト処理に CSRF 対策を行うのは無駄とはいいませんが、なにかしらの被害を抑止することができるものではないということは念頭に入れておいたほうがよいかと思います。
元々、CSRF 対策というのは、全てリクエストに対して行うものではなく、必要な部分に対策を行うものです。今は、全てのリクエストに対して行うべきという風潮がある気がしますが、ちょっと違うのではないでしょうか。
とは言っても、脆弱性対策としての CSRF 対策は必要ないとしても、Web アプリケーション脆弱性診断を受ければ軽微な脆弱性として報告されるでしょうし、昨今のユーザーの目を気にして「CSRF の対策もしてないサイト」という評価を受けたくないこともあるかと思います。
そういう方向けに、CSRF の脆弱性があるサンプルと CSRF 対策を行ったサンプルを提示したいと思います。
CSRF の脆弱性のあるサンプル
サンプルとして提示するのは、ログインとログアウトがある PHP の Web アプリケーションになります。
なお、このサンプルは CSRF 対策にフォーカスを当ててシンプルにしているので、全ての脆弱性対策を行っている訳ではないことにご注意ください。
まずは画面から見ていきましょう。
これがログイン画面 になります。
ログイン後の画面は以下のようになります。
ログアウトするとログイン画面に以下のようにその旨が表示されます。
ではログイン後に罠サイトに移動してみます。
ここで「強制ログアウト」のリンクを踏むと…
見事にログアウトしてしまいましたね。
では CSRF 対策がされる前のソースを見てみましょう。
ソースは、ログイン画面(login.php)→ログイン処理(login_check.php)→Welcome画面(welcome.php)→ログアウト処理(logout.php)、共通関数(function.php)、罠サイト(trap.php)になります。
・ログイン画面(login.php)
<?php require_once("function.php"); session_start(); header("Content-type: text/html; charset=utf-8"); ?> <!doctype html> <html lang="ja"> <body> <h1>ログイン</h1> <?php if ($_SESSION["error_status"] == 1) { echo "<h2 style='color:red'>IDまたはパスワードが異なります。</h2>"; } if ($_SESSION["logout_status"] == 1) { echo "<h2>ログアウトしました。</h2>"; } //ステータス情報のリセット $_SESSION["error_status"] = 0; $_SESSION["logout_status"] = 0; ?> <form action="login_check.php" method="post"> <table border="0"> <tr> <td>ID </td> <td><input type="text" name="id"></td> </tr> <tr> <td> Password </td> <td> <input type="password" name="password"> </td> </tr> </table> <input type="submit" value="ログイン"> <input type="reset" value="リセット"> </form> </body> </html>
・ログイン処理(login_check.php)
<?php require_once("function.php"); session_start(); header("Content-type: text/html; charset=utf-8"); //パラメーター取得 $id = $_POST['id']; $password = $_POST['password']; //簡易ログイン判定 if ($id != "yamada" && $password != "pass") { $_SESSION["error_status"] = 1; header("HTTP/1.1 301 Moved Permanently"); header("Location: login.php"); exit(); } //セッション ID の振り直し session_regenerate_id(true); //セッションに ID を格納 $_SESSION['id'] = $id; //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: welcome.php");
・Welcome画面(welcome.php)
<?php require_once("function.php"); session_start(); header("Content-type: text/html; charset=utf-8"); //エラー情報リセット $_SESSION["error_status"] = 0; ?> <!doctype html> <html lang="ja"> <body> <h1>ようこそ</h1> <a href="trap.php">罠ページへ移動</a> <a href="logout.php">ログアウト</a> </body> </html>
・ログアウト処理(logout.php)
<?php session_start(); header("Content-type: text/html; charset=utf-8"); //セッション破棄 $_SESSION = array(); session_destroy(); //ログアウト情報更新 session_start(); $_SESSION["logout_status"] = 1; //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: login.php");
・共通関数(function.php)
<?php /* * CSRF のトークンを生成 */ function get_csrf_token() { $TOKEN_LENGTH = 16;//16*2=32byte $bytes = openssl_random_pseudo_bytes($TOKEN_LENGTH); return bin2hex($bytes); }
・罠サイト(trap.php)
<?php session_start(); header("Content-type: text/html; charset=utf-8"); ?> <!doctype html> <html lang="ja"> <body> <h1>罠ページ</h1> <a href="http://192.168.33.10/logout.php">強制ログアウト</a> </body> </html>
CSRF 対策を行ったサンプル
CSRF 対策を行うと、罠サイトのリンクを踏んだときに以下の画面が表示されるようになります。なお、この画面のソースはただのHTMLなので省略します。
CSRF 対策は、ログイン時にトークンを作成しセッションに格納します。それからWelcome画面でトークンを含んだログアウト用のフォームを出力します。そして、ログアウト処理時にトークンのチェックを行い、不正なリクエストだったらエラー画面に遷移するようにしています。
なお、ログアウトがリンクになっていて POST リクエストになっているのは、トークンを表に出したくないことと、ボタンよりリンクの方がログアウトの実装が多いだろうという予想からです。
修正したソースを以下に提示します。
・ログイン処理(login_check.php)
<?php require_once("function.php"); session_start(); header("Content-type: text/html; charset=utf-8"); //パラメーター取得 $id = $_POST['id']; $password = $_POST['password']; //簡易ログイン判定 if ($id != "yamada" && $password != "pass") { $_SESSION["error_status"] = 1; header("HTTP/1.1 301 Moved Permanently"); header("Location: login.php"); exit(); } //セッション ID の振り直し session_regenerate_id(true); //セッションに ID を格納 $_SESSION['id'] = $id; //CSRF のトークン作成 $_SESSION["token"] = get_csrf_token(); //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: welcome.php");
・Welcome画面(welcome.php)
<?php require_once("function.php"); session_start(); header("Content-type: text/html; charset=utf-8"); //エラー情報リセット $_SESSION["error_status"] = 0; ?> <!doctype html> <html lang="ja"> <body> <h1>ようこそ</h1> <a href="trap.php">罠ページへ移動</a> <a href="javascript:void()" onclick="logout.submit();">ログアウト</a> <form name="logout" action="logout.php" method="post"> <input type="hidden" name="token" value="<?php echo htmlspecialchars($_SESSION["token"] , ENT_QUOTES, "UTF-8") ?>"> </form> </body> </html>
・ログアウト処理(logout.php)
<?php session_start(); header("Content-type: text/html; charset=utf-8"); $token = $_POST["token"]; //CSRF チェック if ($token != $_SESSION["token"]) { //エラー画面に遷移 header("HTTP/1.1 301 Moved Permanently"); header("Location: error.php"); exit(); } //セッション破棄 $_SESSION = array(); session_destroy(); //ログアウト情報更新 session_start(); $_SESSION["logout_status"] = 1; //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: login.php");
おわりに
ログアウトの CSRF 対策の必要性の考察と、CSRF 対策の具体例を提示してみましたがいかがだったでしょうか。
一部にはログアウト時にも CSRF 対策をしないと安全な Web アプリケーションとはいえないという意見もあるかと思います。ですが、なんでもかんでも思考停止で CSRF 対策を行うのはよくないと思いこの記事を書きました。
CSRF 対策についてよく考えられていれば、トークンでの対策だけではなく、重要な箇所では再度ログインさせるという対策の発想も浮かんでくると思います。
この記事が CSRF 対策の役に立てば幸いです。
コメント