kikki's tech note

技術ブログです。UnityやSpine、MS、Javaなど技術色々について解説しています。

ASP.NET MVC(C#)でCSRF Tokenを発行する

本章では、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対策のまとめ2012年版 - 不意になにかを残すブログ

今回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を発行する」でした。


※無断転載禁止 Copyright (C) kikkisnrdec All Rights Reserved.