2020-01-07 更新

reactで作成したWebアプリケーションのCSRF対策

ASP.NET coreでreactベースのプロジェクトテンプレートを使った際のCSRF(XSRF)対策です。

CSRF(クロスサイト リクエスト フォージェリ―)とは、ログイン中のユーザをターゲットにユーザが意図しない(悪意のある)リクエストを別ドメインのサイトから送り、パスワード変更のリクエストなど、場合によってはアカウントを乗っ取ろうとする攻撃のことです。

目次

  • 通常時のCSRF対策
  • Reactベースで作成したWebアプリのCSRF対策
  • 最後に
  • 参考リンク

通常時のCSRF対策

reactやAngularを使っていない場合は、以下のようなコードで簡単に実装できます。

ビュー(クライアント側)。


<form action="/Sample/Register" method="post">
    <button type="submit"></button>
</form>

ASP.NET Core 2.0からかもしれませんが、上記のコードだけで判定用のトークンが自動で出力されます。(手動で出力する場合は「@Html.AntiForgeryToken()」を使用)

実際にブラウザに表示されたコードを見るとトークンが以下のように出力されています。


<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAk**********" />            

コントローラー(サーバ側)では属性に「ValidateAntiForgeryToken」を付加しておきます。


public class SampleController : Controller
{
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Register(){
    }
}

上記の対応だけでsubmitされた時に一緒にトークンが送られ、その有効性をチェックして、有効でないリクエストはエラーとして処理してくれます。

通常時のCSRFへの対応はこれだけなのですが、ReactやAngularを使ったWebサイトの場合、サーバ側とのやりとりがREST APIで行われるような作りとなるため、上記の対応ではうまくいきません。

公式のドキュメントにAngularやJavaScriptでの対応方法が掲載されていますが、ReactベースのWebアプリでどう対応すればいいのか分からなかったので、以下はその時のメモになります。

Reactベースで作成したWebアプリのCSRF対策

ReactベースのWebアプリのCSRF対策ですが、基本的には公式ドキュメントのJavaScriptの例と同じようにします。

コードはASP.NET CoreのReact + Reduxベースのプロジェクトテンプレートを利用したものになるので、適宜、読み替えてください。


まずはビュー側(Views/Home/Index.cshtml)。


@{
    ViewData["Title"] = "Home Page";
}
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
    public string GetAntiXsrfRequestToken()
    {
        return Xsrf.GetAndStoreTokens(Context).RequestToken;
    }
}
<div id="react-app" asp-prerender-module="ClientApp/dist/main-server">Loading...</div>
<input type="hidden" name="CSRF-TOKEN" value="@GetAntiXsrfRequestToken()" />

@section scripts {
    <script src="~/dist/main-client.js" asp-append-version="true"></script>
}

公式ドキュメントに沿って対応しましたが、通常時と同様に「@Html.AntiForgeryToken()」でやっても大丈夫でした。違いはフィールド名が「__RequestVerificationToken」になるか、こちらで命名したものになるかの違いだけです。


続いてサーバ側ですが、まずStartUp.csに以下のコードを追加します。


public class Startup
{
     public void ConfigureServices(IServiceCollection services)
     {
         services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
     }
}

上記のコードはCSRFのチェック方法を指定しています。この場合、リクエストのヘッダー情報にある「X-CSRF-TOKEN」というキーに判定用のトークンが格納されていることを示しています。

コントローラーは通常時と同じです。


public class SampleController : Controller
{
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Register(){
    }
}

最後にビュー側に配置したトークンをリクエストのヘッダーに含める際のコードです。

React + Reduxベースのプロジェクトの場合、actionCreatorsにリクエストを作成します。


export const actionCreators = {
    requestSampleRegister: (): AppThunkAction<KnownAction> => (dispatch, getState) => {
        let fetchTask = fetch(`api/Sample/Register`, {
            method: 'POST',
            credentials: 'same-origin',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                "X-CSRF-TOKEN": document.getElementsByName("CSRF-TOKEN").item(0).getAttribute('value');
            },
            body: JSON.stringify({id: "1", name:"sample data1"})
        })
            .then(response => response.json())
            .then(data => {
                dispatch({ type: 'RECEIVE_SAMPLE_REGISTER'});
            });
        addTask(fetchTask);
        dispatch({ type: 'REQUEST_SAMPLE_REGISTER' });
    }
};

注意点としては、「credentials: 'same-origin'」が必要な点です。

fetchメソッドを使う場合、デフォルトでは現在のドメインのCookieが自動で送信されないので、この設定が必要です。設定していなかった場合、以下のようなエラーが発生します。

Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The required antiforgery cookie ".AspNetCore.Antiforgery.***********" is not present.

トークンの整合性チェックはCookieで保持しているトークンと上記のようにリクエストのヘッダーに埋め込んだ2つのトークンで判定しているため、このような問題が発生します。

自分はこれが分からずに結構ハマってしまいました。

最後に

とりあえず動作確認はOKでしたが、本当にこの対応で問題ないのか...という不安がないこともないので、問題に気がついたり、他にいい方法があったらまた報告します。

参考リンク

ASP.NET Core】関連記事