本章では、ASP.NET MVCで同期・非同期(Ajax)に関わらず、認証過程でCSRF Tokenを発行する方法について共有します。
CSRF
CSRFとは
CSRFは、Wikipediaで以下のように定義されています。
クロスサイトリクエストフォージェリ(Cross site request forgeries、略記:CSRF、またはXSRF)は、WWW における攻撃手法のひとつである。
クロスサイトリクエストフォージェリ - Wikipedia
そこでWebサイト運営者は、悪意あるユーザが別のユーザになれないよう、Webサイトを調整する必要があります。
CSRF対策
CSRFを防ぐ方法として、以下の記事があります。
- 何らかの処理を起動するような、ユーザのクリック(=意思あるいは同意)を確認すべき全ての画面遷移とその前後で(ログインパスワード入力フォームを含む)で、ワンタイムトークンを発行・検証する。
- ワンタイムトークンはformのhidden要素などとしてページ内に埋め込む。cookieなどブラウザグローバルなローカルストレージに格納してはならない
- ワンタイムトークンは、ログインクッキーなどで判別したユーザーと一致すること必ず確認する
- X-frame-options: deny (あるいはsameorigin)ヘッダを設定する
- JSONなどjavscriptとして解釈されうるページに秘密情報を載せたりJSONPへのアクセスを破壊的操作に利用しない。
今回CSRF対策として、ASP.NET MVC上で実施する方法について解説します。
プログラム
ヘルパークラスの準備
まず、サーバー側で認証時にCSRFトークンをチェックする仕組みを用意します。以下のプログラムを準備します。
[ValidateAntiForgeryTokenOnAllPosts.cs]
[AttributeUsage(AttributeTargets.Class)] public class ValidateAntiForgeryTokenOnAllPosts : AuthorizeAttribute { public override void OnAuthorization(AuthorizationContext filterContext) { string cookieToken = ""; string formToken = ""; var resolveRequest = filterContext.HttpContext.Request; if (resolveRequest.HttpMethod == WebRequestMethods.Http.Post) { if (resolveRequest.IsAjaxRequest()) { var token = resolveRequest.Headers.GetValues("__RequestVerificationToken").FirstOrDefault(); var splitToken = token.Split(':'); if (splitToken.Length == 2) { cookieToken = splitToken[0].Trim(); formToken = splitToken[1].Trim(); } AntiForgery.Validate(cookieToken, formToken); } else { new ValidateAntiForgeryTokenAttribute().OnAuthorization(filterContext); } } } }
上記ヘルパークラスの特徴として、同期処理と非同期処理両方で検証方法を用意しています。同期処理では、元々用意されているCSRF確認機構を利用し、非同期処理では独自にチェックします。
実用例
それでは、実際に利用してみましょう。以下サンプル例です。
[LoginController.cs]
// 以下の属性をControllerクラスに追加するだけで、POSTされるリクエストに対して検証されます。 [ValidateAntiForgeryTokenOnAllPosts] public class LoginController : Controller { [HttpPost] public async Task<ActionResult> ValidateUser(string Id, string Password) { } }
同期(ページ遷移)処理の場合
同期処理では、ASP.NET MVCで用意されているCSRF Token発行ヘルパーを利用するだけで問題ありません。
[login1.html]
<!DOCTYPE html> <html> <head> <title>login page</title> </head> <body> @using (@Html.BeginForm("Index", "Login")) { @Html.AntiForgeryToken(); <label><span>メールアドレス</span>@Html.TextBoxFor(p => p.Id)</label> <label><span>パスワード</span>@Html.PasswordFor(p => p.Password)</label> <input id="btnSendLogin" type="submit" value="ログイン" /> } </body> </html>
非同期(Ajax)処理の場合
非同期処理では、クッキーのトークンとフォームのトークンを発行し、POST時にヘッダに追加してリクエストします。
[login2.html]
<!DOCTYPE html> <html> <head> <title>login page</title> </head> <body> <label><span>メールアドレス</span>@Html.TextBoxFor(p => p.Id)</label> <label><span>パスワード</span>@Html.PasswordFor(p => p.Password)</label> <input id="btnSendLogin" type="submit" value="ログイン" /> <script> @functions{ public string TokenHeaderValue() { string cookieToken, formToken; AntiForgery.GetTokens(null, out cookieToken, out formToken); return cookieToken + ":" + formToken; } } var checkObject= "@TokenHeaderValue()"; </script> </body> </html>
[login2.js]
document.getElementById("submit").addEventListener("click", function () { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { switch (xhr.readyState) { case 1: //request start break; case 2: //recieved header data break; case 3: //reciving body data break; case 4: //complete xhr connect switch (xhr.status) { case 200: //success var _recieveData = JSON.parse(xhr.responseText); alert(_recieveData); break; default: //error break; } break; } }; xhr.open("POST", "/Login/ValidateUserAsync"); xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.setRequestHeader("__RequestVerificationToken", checkObject); xhr.send(JSON.stringify({ Id: document.getElementById("Id").value, Password: document.getElementById("Password").value, })); });
総括
Webサイトへの悪意ある攻撃が増加の一途を辿っています。今回紹介したセキュリティ対策は、数ある中の一部に過ぎません。そのため、少しでもセキュリティ対策を心がけるよう努めていきましょう。
以上、「ASP.NET MVC(C#)でCSRF Tokenを発行する」でした。