Razor を利用した MVC での CSRF 対策
ここではセキュリティ対策のひとつとして、クロスサイトリクエストフォージェリ (Cross-site Request Forgery, CSRF または XSRF) 対策について説明します。
CSRF がどのようなものであるか、ということについては、説明が長くなるのでここでは説明を割愛します。
しかしながら、CSRF 対策として「そのセッションだけに有効なトークンをフォームに埋め込むことが有効である」ことだけは、頭に入れて置いてください。
それを前提として、ASP.NET の MVC (Razor ビュー) でアンチ・クロスサイトリクエストフォージェリ・トークンをどのように埋め込むのか、そして、それをどのように使うのか説明します。
CSRF 対策を施していない場合の動作確認
まずはじめに、何も CSRF 対策を実施していない場合の挙動を確認しましょう。
ASP.NET の MVC Web アプリケーションを次のように構成します。
モデル: Employee.cs
namespace MvcApplication1.Models {
public class Employee {
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}
ビュー: Employee/Create.cshtml
@model MvcApplication1.Models.Employee
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Create</title>
</head>
<body>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Employee</legend>
<div class="editor-label">
@Html.LabelFor(model => model.FirstName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.FirstName)
@Html.ValidationMessageFor(model => model.FirstName)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Email)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Email)
@Html.ValidationMessageFor(model => model.Email)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
</body>
</html>
コントローラ: EmployeeController.cs
using MvcApplication1.Models;
using System.Web.Mvc;
namespace MvcApplication1.Controllers {
public class EmployeeController : Controller {
[HttpGet]
public ActionResult Create() {
return View();
}
[HttpPost]
public ActionResult Create(Employee employee) {
return View();
}
}
}
/Employee/Create を要求すると、次のようにフォームが現れます。
試しに開発環境でコントローラのアクションメソッドにブレークポイントを設定しておき、フォームからデータをポストしてみましょう。
フォームにデータを入力して、Submit をクリックすると・・・
確かにブレークポイントがヒットしました。
コントローラでは中身も確かに受け取れていますね。
さて、このフォームの POST をブラウザからではなく、他のクライアントから行なってみましょう。いきなり POST を送ったときに、 ウェブアプリケーション側がどのような挙動を示すかみるためです。
ここで、マイクロソフトから無償でダウンロードできる WFetch というテストツールを利用します。
画面左上側でホスト名、パス、ポート番号、HTTP メソッドなどを指定します。
画面右上側で追加の HTTP ヘッダーや HTTP ボディを追加します。
ここではフォームデータと認識させるために Content-Type に application/x-www-form-urlencoded を指定しています。
ポストする内容は FirstName として "A"、LastName として "B"、Email として "C" という文字を設定します。Content-Length はツールが自動的に追加してくれるので自分で文字数を数える必要はありません。
WFetch の "Go" ボタンをクリックしてリクエストを送信すると、サーバー側では確かに先ほどと同じアクションメソッドのブレークポイントがヒットします。
中身もコントローラで読めていることが確認できますね。さらに処理を継続すると、クライアント側 (Wfetch) に HTML が返されています。
このように、Wfetch からいきなり POST データを送りつけても、問題なく処理が行われたことがわかります。
CSRF 対策・アンチ・クロスサイトリクエストフォージェリトークンの埋め込み
ポイントは正しい HTML フォームからの処理ではないにも関わらず、サーバー側で処理が行なわれたところです。セキュリティを確保するには、 想定したコンテキストでブラウザに表示された HTML からのポストデータのみを処理したいわけです。このために、CSRF トークンをフォームに埋め込み ポストデータに含ませて、サーバー側で認識しているセッション情報と照合します。
ASP.NET MVC ではトークンの埋め込みをサポートしています。
フォームへの CSRF 対策トークンの埋め込みとトークンの検証
Razor では @Html.AntiForgeryToken() を呼ぶだけで、トークンを埋め込むことができます。
トークンの検証を行なうには、コントローラのアクションメソッドに ValidateAntiForgeryToken 属性を指定します。
上記を設定しておくことによって、アクションメソッドはトークンを要求し、フォームにはトークンを埋め込むことができます。
試しに上記の方法で Wfetch からリクエストを送ると、次のスクリーンショットのように例外が発生して処理が停止されていることが分かります。
ブラウザからの要求であっても、次のようにトークンを書き換えると・・・
次のように HttpAntiForgeryException が発生して処理が停止します。
尚、もちろん小細工せずに HTML フォームからポストしたデータは問題なく処理されます。
エラーをアプリケーションで処理する方法
セキュリティ対策としては、上記のように処理を停止してしまえば十分ですが、エラー画面が見苦しいのも確かです。
サーバーの設定としては、サーバーインターナルエラー "500" が出るので、その場合のカスタムエラーページを設定すれば OK です。
その他は、アプリケーション側で HttpAntiForgeryException を処理する場合は、Global.asax の Application_Error メソッドで Server.GetLastError() を用いて発生した例外を取得しその型をチェックして、HttpAntiForgeryException の場合の特別な処理を入れる方法などがあります。
いずれにせよ、大事なのは不正な処理を実行しないというところにあり、美しいエラーページを表示することではないのでここまでする必要は通常ないでしょう。