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でしたが、本当にこの対応で問題ないのか...という不安がないこともないので、問題に気がついたり、他にいい方法があったらまた報告します。