ASP.NET SPAサイトにCSRF(XSRF)対策を実装する
ASP.NETのSPAプロジェクトにクロスサイトリクエストフォージェリ(CSRF, XSRF)対策を実装する方法を紹介します。
前提
SPAのプロジェクトなので、初期表示時(リロード時含む)以外の通信はajaxになります。
ajaxでアクセスするURLはhttps://zukucode.com/api/xxxのように必ずパスに/apiが付いているものとします。
逆に初期表示時などに使用する通常のURLはhttps://zukucode.com/loginのように/apiはつかないものとします。
XSRFトークン出力
Startup.csで、XSRFトークンを出力するように実装します。
以下のようにXSRFトークンを出力して、クライアント側にcookieとしてXSRFトークンがセットされます。
Startup.cs(一部抜粋)using Microsoft.AspNetCore.Antiforgery;
// 引数にIAntiforgery antiforgeryを追加
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
app.Use(next => context =>
{
string path = context.Request.Path.Value;
// 通常アクセス時のみXSRFトークンを出力(ajax時は出力しない)
if (!path.ToLower().Contains("/api"))
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions() {
HttpOnly = false,
Secure = true
});
}
return next(context);
});レスポンスヘッダの内容を確認すると、以下のような項目が出力されていることを確認できます。
set-cookie: XSRF-TOKEN=文字列; path=/; secure
CSRF保護
リクエスト時に、上記で出力したXSRFトークンがリクエストヘッダに含まれていない場合はエラーになるように設定します。
コントローラーでURLごとに設定する方法もありますが、今回はアプリケーション全体でCSRF保護の設定を行うようにします。
Startup.csのConfigureServicesファンクションに以下の処理を追加します。
リクエストヘッダのXSRFトークンの名前はX-XSRF-TOKENとします。
なぜX-XSRF-TOKENという名前にするかというと、Angularやaxiosなどのクライアントのajaxライブラリでは、レスポンスされたXSRF-TOKEN(cookieのXSRF-TOKEN)の値を、リクエスト時にX-XSRF-TOKENという名前でリクエストヘッダに含める動作を自動的に行っているためです。
トークンの名前を合わせておけば、クライアント側でリクエストヘッダにXSRFトークンを追加するような処理を実装する必要がなくなります。
Startup.cs(一部抜粋)public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(options =>
{
// リクエストヘッダのXSRFトークンの名前
options.HeaderName = "X-XSRF-TOKEN";
});
services.AddControllersWithViews(options =>
{
// すべてのリクエストに対してCSRF保護を設定
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});動作確認
プロジェクトを作成した際の雛形として作成されたWeatherForecastControllerで動作確認を行います。
ここではAPIとしてajaxでアクセスするため、Routeの設定箇所に以下のようにapiを追加します。
こうすると、api/WeatherForecastでアクセスできるようになります。
[ApiController]
[Route("[controller]")]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBaseまた、GETリクエストではCSRF保護の動作確認はできないため、POSTリクエストに変更します。
[HttpGet]
public IEnumerable<WeatherForecast> Get()
[HttpPost]
public IEnumerable<WeatherForecast> Post()クライアント側で、ajaxのリクエストを投げます。
ここではajaxのライブラリにaxiosを使用します。
上述したように、axiosはcookieのXSRF-TOKENをX-XSRF-TOKENという名前でリクエストヘッダに自動でセットしていますので、リクエストヘッダにトークンを追加する処理は不要です。
const response = axios.post('/api/WeatherForecast');上記のリクエストのヘッダにX-XSRF-TOKEN: 文字列が含まれていることを確認でき、データがレスポンスされていることも確認できます。
試しに上記の「XSRFトークン出力」の処理をすべてコメントアウトして試してみます。
また、ブラウザに保存されているXSRFトークンのcookieは削除しておきます。
その状態でアクセスをすると400(Bad Request)のエラーになることが確認できます。
これによりトークンが存在しないリクエストは拒否されることが確認できます。