Visual Studio のテンプレートを使って作成する ASP.NET MVC と Web Forms プロジェクトに組み込まれている Cross-Site Request Forgery (CSRF) 対策について書きます。
上の図は、Microsoft のドキュメント「ASP.NET Core でクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を防止する」に述べられている CSRF 攻撃の仕組みを図示したものです。
どういうことかと言うと、(1) ユーザー認証が必要な正規サイトにログインしたクライアントが、ログアウトせず(即ち、認証クッキーを持ったまま)作業を続ける。(2) その後、悪意のあるサイトにアクセスして攻撃用の HTML フォームが仕組まれたページを閲覧し、その攻撃用の HTML フォームのボタンをクリックする。(3) それにより正規サイトに POST 要求がかかるが、認証クッキーも同時に送信されるので、認証ユーザーが実行を許可されているアクションを実行できる・・・ということです。
その攻撃を防止するための組み込みの機能について以下に書きます。
(1) MVC の場合
Microsoft のドキュメント「Web Stack Runtime XSRF の軽減策」に書いてあるように、2 つの CSRF 対策用トークンを用います。
まず、サーバが入力フォームをクライアントに送信する際、クッキーと隠しフィールドに CSRF 対策用トークンを設定してクライアントに渡します。
クライアントが入力フォーム上で入力を済ませて、サーバーにフォームを POST 送信する際、同時にクッキーと隠しフィールドの CSRF 対策用トークンもサーバーに送信されます。
サーバーは、両方のトークンが比較チェックに合格した場合にのみ要求の続行を許可します。不合格の場合はサーバーエラーとなります。
上の図で、「罠を仕込んだページ」の「攻撃用の HTML フォーム」には CSRF 対策用トークンを持った隠しフィールドは存在しないので比較チェックは不合格となり、CSRF 攻撃を防ぐことができるということになります。
(2) Web Forms の場合
Web Froms には状態管理の手段の一つに ViewState というものがあって、隠しフィールドに状態を保存してクライアントに送信し、ポストバックされたときにサーバー側で隠しフィールドから状態情報を取得するという仕組みになっています。
ViewState には EnableViewStateMac という改ざんを検証する機能があって、デフォルトで有効になっているので、ある程度それで CSRF を防ぐことができます。
ただ、それだけでは不十分だそうで、Page.ViewStateUserKey プロパティを利用して、ViewState に個々のユーザーの 識別子を割り当てることが推奨されています。
ViewStateUserKey プロパティを設定した場合、ASP.NET は、ポストバックによってクライアントから送信されてきた隠しフィールドの ViewState から識別子を抽出し、実行中のページの ViewStateUserKey と比較します。 2 つが一致する場合要求は正当と見なされ、それ以外の場合は例外がスローされる仕組みになっているそうです。(そのあたりの詳しい説明は ViewStateUserKey を見てください)
Visual Studio 2022 のテンプレートで作る Web アプリケーションプロジェクトの場合、マスターページ (Site.Master.cs) に、Page.ViewStateUserKey プロパティを利用して CSRF 対策を強化するためのコードが実装されています。
それがどうなっているかを説明します。
上の Microsoft のドキュメントでは Session.SessionID を Page.ViewStateUserKey プロパティに設定する例が紹介されています。しかし、Session を使わない限り Session cookie が発行されないので、そうはできないケースがあります。
なので、Site.Master.cs では、SessionID に代えて Guid の文字列を Page.ViewStateUserKey プロパティに設定しています。そのコードは以下の通りです。
Page_Init で、要求ヘッダに __AntiXsrfToken という名前のクッキーが含まれ、かつ、その値を Guid にパースできる場合、そのクッキーの値を Page.ViewStateUserKey に設定しています。
要求ヘッダに __AntiXsrfToken という名前のクッキーが含まれない場合、含まれていてもその値を Guid にパースできない場合は、Guid を生成してその文字列を Page.ViewStateUserKey に設定します。さらに、__AntiXsrfToken という名前のクッキーを作成し、その値に生成した Guid の文字列を設定してクライアントに送信しています。
using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.AspNet.Identity;
namespace WebForms1
{
public partial class SiteMaster : MasterPage
{
private const string AntiXsrfTokenKey = "__AntiXsrfToken";
private const string AntiXsrfUserNameKey = "__AntiXsrfUserName";
private string _antiXsrfTokenValue;
protected void Page_Init(object sender, EventArgs e)
{
// 以下のコードは、XSRF 攻撃からの保護に役立ちます
var requestCookie = Request.Cookies[AntiXsrfTokenKey];
Guid requestCookieGuidValue;
if (requestCookie != null &&
Guid.TryParse(requestCookie.Value,
out requestCookieGuidValue))
{
// Cookie の Anti-XSRF トークンを使用します
_antiXsrfTokenValue = requestCookie.Value;
Page.ViewStateUserKey = _antiXsrfTokenValue;
}
else
{
// 新しい Anti-XSRF トークンを生成し、Cookie に保存
_antiXsrfTokenValue = Guid.NewGuid().ToString("N");
Page.ViewStateUserKey = _antiXsrfTokenValue;
var responseCookie = new HttpCookie(AntiXsrfTokenKey)
{
HttpOnly = true,
Value = _antiXsrfTokenValue
};
if (FormsAuthentication.RequireSSL &&
Request.IsSecureConnection)
{
responseCookie.Secure = true;
}
Response.Cookies.Set(responseCookie);
}
Page.PreLoad += master_Page_PreLoad;
}
// ・・・中略・・・
}
}
さらに、初期画面の要求の処理を行う際 Page_PreLoad で ViewState に Page.ViewStateUserKey の文字列とユーザー名(ユーザーが認証を受けている場合)を設定し、ポストバックの際はそれらが一致しているか否かの検証を行い一致しない場合は例外をスローするという操作を、上のコードで Page.PreLoad にアタッチしたイベントハンドラ master_Page_PreLoad で行っています。
そのコードは以下の通りです。
protected void master_Page_PreLoad(object sender, EventArgs e)
{
if (!IsPostBack)
{
// Anti-XSRF トークンを設定します
ViewState[AntiXsrfTokenKey] = Page.ViewStateUserKey;
ViewState[AntiXsrfUserNameKey] =
Context.User.Identity.Name ?? String.Empty;
}
else
{
// Anti-XSRF トークンを検証します
if ((string)ViewState[AntiXsrfTokenKey] != _antiXsrfTokenValue ||
(string)ViewState[AntiXsrfUserNameKey] !=
(Context.User.Identity.Name ?? String.Empty))
{
throw new InvalidOperationException(
"Anti-XSRF トークンの検証が失敗しました。");
}
}
}
ただし、上に書いた「攻撃用の HTML フォーム」で POST 要求するような場合は ViewState が含まれないので ViewState Mac の検証機能は動きません。また、ポストバックと判定されないので master_Page_PreLoad によるトークンの検証もされません。
それで CSRF 対策になるのかというのが疑問でしたが、普通に ASP.NET Web Forms アプリでやるように、ユーザー入力に TextBox コントロール使い、Button コントロールをクリックしてポストバックし、Button の Click イベントで処置を行うケースでは問題なさそうです。
「攻撃用の HTML フォーム」から送信された値は TextBox.Text に代入されることはなく(TextBox.LoadPostData が呼ばれないので)、Click イベントも発生しないのでサーバー側では何も起こりません。
ちょっと問題なのは「攻撃用の HTML フォーム」は POST 要求を出すのでそれを受けた正規 Website は応答を返すという点です。予期せぬ応答をもらったユーザーはびっくりすると思いま��が、その対策までは上のコードには実装されていません。