ASP.NET Core でクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を防止する

作成者: Fiyaz HasanRick Anderson

クロスサイト リクエスト フォージェリは、Web ホスト アプリに対する攻撃であり、悪意のある Web アプリがクライアント ブラウザーとそのブラウザーを信頼する Web アプリとの間の対話に、影響を与える可能性があります。 これらの攻撃は、Web ブラウザーが Web サイトへのすべての要求に対して何らかの種類の認証トークンを自動的に送信するために発生する可能性があります。 この形式の悪用は、攻撃でユーザーの以前に認証されたセッションが利用されるため、"ワンクリック攻撃" または "セッション ライディング" としても知られます。 クロスサイト リクエスト フォージェリは、XSRF または CSRF とも呼ばれます。

CSRF 攻撃の例:

  1. ユーザーが、フォーム認証を使用して www.good-banking-site.example.com にサインインします。 サーバーはユーザーを認証し、認証 cookie を含む応答を発行します。 サイトは、有効な認証 cookie と共に受信したすべての要求を信頼するため、攻撃に対して脆弱です。

  2. ユーザーが悪意のあるサイト www.bad-crook-site.example.com にアクセスします。

    悪意のあるサイト www.bad-crook-site.example.com には、次の例のような HTML フォームが含まれています。

    <h1>Congratulations! You're a Winner!</h1>
    <form action="https://good-banking-site.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click to collect your prize!" />
    </form>
    

    悪意のあるサイトではなく、脆弱なサイトにフォームの action が POST されていることに注意してください。 これが CSRF の "クロスサイト" 部分です。

  3. ユーザーが送信ボタンを選びます。 ブラウザーは要求を行い、要求されたドメイン www.good-banking-site.example.com に対する認証 cookie を自動的に含めます。

  4. 要求はユーザーの認証コンテキストを使用して www.good-banking-site.example.com サーバー上で実行され、認証されたユーザーが実行を許可されている任意のアクションを実行できます。

ユーザーがボタンを選んでフォームを送信するシナリオに加えて、悪意のあるサイトでは次のこともできます。

  • フォームを自動的に送信するスクリプトを実行する。
  • フォーム送信を AJAX 要求として送信する。
  • CSS を使用してフォームを非表示にする。

これらの代替シナリオでは、最初に悪意のあるサイトにアクセスすること以外に、ユーザーによる操作や入力は必要ありません。

HTTPS を使用しても CSRF 攻撃を防ぐことはできません。 悪意のあるサイトは、安全ではない要求を送信するのと同じように簡単に、https://www.good-banking-site.com/ 要求を送信できます。

一部の攻撃は GET 要求に応答するエンドポイントを対象にしており、その場合、イメージ タグを使用してアクションを実行できます。 この形式の攻撃は、イメージは許可するが JavaScript はブロックするフォーラム サイトでよく使用されます。 GET 要求で状態を変更するアプリ (変数またはリソースが変更される場合) は、悪意のある攻撃に対して脆弱です。 状態を変更する GET 要求は安全ではありません。 ベスト プラクティスは、GET 要求で状態を変更しないことです。

次の理由により、認証に cookie を使用する Web アプリに対して、CSRF 攻撃が行われる可能性があります。

  • Web アプリによって発行された cookie がブラウザーで格納されます。
  • 格納される cookie には、認証されたユーザーのセッション cookie が含まれます。
  • アプリへの要求がブラウザー内で生成された方法に関係なく、ブラウザーからは、ドメインに関連付けられているすべての cookie が要求ごとに Web アプリに送信されます。

ただし、CSRF 攻撃は、cookie の悪用に限定されません。 たとえば、基本認証やダイジェスト認証も脆弱です。 ユーザーが基本認証またはダイジェスト認証を使用してサインインすると、セッションが終了するまで、ブラウザーによって資格情報が自動的に送信されます。

このコンテキストでの "セッション" とは、その間ユーザーが認証されている、クライアント側のセッションを指します。 サーバー側セッションや ASP.NET Core のセッション ミドルウェアとは無関係です。

ユーザーは、予防措置を講じて CSRF の脆弱性を防ぐことができます。

  • Web アプリを使い終わったら、それからサインアウトします。
  • ブラウザーの cookie を定期的にクリアします。

ただし、CSRF の脆弱性は、根本的に、エンド ユーザーではなく Web アプリに関する問題です。

認証の基礎

Cookie ベースの認証は、一般的な認証形式です。 トークン ベースの認証システムは普及が広がっています (特にシングルページ アプリケーション (SPA) の場合)。

ユーザーがユーザー名とパスワードを使用して認証を行うと、認証チケットが含まれるトークンが発行されます。 このトークンは、認証と承認に使用できます。 トークンは、クライアントが行うすべての要求で送信される cookie として格納されます。 この cookie の生成と検証は、Cookie 認証ミドルウェアを使用して行われます。 ミドルウェアによって、ユーザー プリンシパルは暗号化された cookie にシリアル化されます。 後続の要求では、ミドルウェアによって cookie が検証され、プリンシパルが再作成されて、そのプリンシパルが HttpContext.User プロパティに割り当てられます。

トークンベースの認証

認証されたユーザーには、トークンが発行されます (偽造防止トークンではありません)。 このトークンには、クレームまたはアプリで保持されているユーザー状態をアプリに示す参照トークンの形式で、ユーザー情報が含まれます。 ユーザーが認証を必要とするリソースにアクセスしようとすると、ベアラー トークンの形式の追加承認ヘッダーと共にトークンがアプリに送信されます。 この方法により、アプリはステートレスになります。 後続の要求ごとに、サーバー側の検証のために要求でトークンが渡されます。 このトークンは、"暗号化" されておらず、"エンコード" されています。 サーバーでは、トークンをデコードして情報にアクセスします。 後続の要求でトークンを送信するため、トークンはブラウザーのローカル ストレージに格納されます。 トークンをブラウザーのローカル ストレージに配置して取得し、ベアラー トークンとして使用すると、CSRF 攻撃に対する保護が提供されます。 ただし、XSS または侵害された外部 JavaScript ファイルを介したスクリプト インジェクションに対してアプリが脆弱な場合、攻撃者はローカル ストレージから任意の値を取得して自分自身に送信する可能性があります。 ASP.NET Core では、既定ですべてのサーバー側出力が変数からエンコードされるため、XSS のリスクが軽減されます。 Html.Raw を使用するか、信頼されていない入力でカスタム コードを使用してこの動作をオーバーライドすると、XSS のリスクが高くなる可能性があります。

トークンがブラウザーのローカル ストレージに格納される場合は、CSRF の脆弱性について心配しないでください。 CSRF が問題になるのは、トークンが cookie に格納さるときです。 詳しくは、GitHub のイシュー「SPA code sample adds two cookies (SPA のコード サンプルで 2 つの cookie が追加される)」をご覧ください。

1 つのドメインでホストされている複数のアプリ

共有ホスティング環境は、セッション ハイジャック、サインイン CSRF、その他の攻撃に対して脆弱です。

example1.contoso.netexample2.contoso.net は異なるホストですが、*.contoso.net ドメインの下のホスト間には暗黙的な信頼関係があります。 この暗黙的な信頼関係により、信頼できないホストが互いの cookie に影響を与えることができる可能性があります (AJAX 要求を管理する同一オリジン ポリシーは、必ずしも HTTP cookie に適用されるとは限りません)。

同じドメインでホストされているアプリ間で信頼された cookie を悪用する攻撃は、ドメインを共有しないことで回避できます。 各アプリが独自のドメインでホストされている場合、悪用するための暗黙的な cookie の信頼関係はありません。

ASP.NET Core での偽造防止

警告

ASP.NET Core では、ASP.NET Core データ保護を使用して偽造防止が実装されています。 サーバー ファームで動作するように、データ保護スタックを構成する必要があります。 詳しくは、データ保護の構成に関する記事をご覧ください。

次のいずれかの API が Program.cs で呼び出されると、偽造防止ミドルウェアが依存関係挿入コンテナーに追加されます。

詳細については、「最小限の API を使用した偽造防止」を参照してください。

FormTagHelper により HTML フォーム要素に偽造防止トークンが挿入されます。 Razor ファイルの次のマークアップによって、偽造防止トークンが自動的に生成されます。

<form method="post">
    <!-- ... -->
</form>

同様に、フォームのメソッドが GET でない場合は、IHtmlHelper.BeginForm によって既定で偽造防止トークンが生成されます。

HTML フォーム要素に対する偽造防止トークンの自動生成は、<form> タグに method="post" 属性が含まれていて、次のいずれかが当てはまる場合に行われます。

  • action 属性が空です (action="")。
  • action 属性が指定されていません (<form method="post">)。

HTML フォーム要素に対する偽造防止トークンの自動生成は、無効にすることができます。

  • 偽造防止トークンを明示的に無効にするには、asp-antiforgery 属性を使います。

    <form method="post" asp-antiforgery="false">
        <!-- ... -->
    </form>
    
  • タグ ヘルパーの ! オプトアウト シンボルを使うと、フォーム要素がタグ ヘルパーからオプトアウトされます。

    <!form method="post">
        <!-- ... -->
    </!form>
    
  • ビューから FormTagHelper を削除します。 Razor ビューに次のディレクティブを追加することで、ビューから FormTagHelper を削除できます。

    @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
    

Note

Razor Pages は XSRF/CSRF から自動的に保護されます。 詳細しくは、「XSRF/CSRF および Razor ページ」をご覧ください。

CSRF 攻撃を防ぐ最も一般的な方法は、"シンクロナイザー トークン パターン" (STP) を使うことです。 STP は、ユーザーがフォーム データを含むページを要求するときに使われます。

  1. サーバーは、現在のユーザーの ID に関連付けられているトークンをクライアントに送信します。
  2. クライアントは、検証のためにトークンをサーバーに送り返します。
  3. サーバーが受け取ったトークンが、認証されたユーザーの ID と一致しない場合、要求は拒否されます。

トークンは一意で、予測できません。 トークンは、一連の要求の適切な順序を保証するためにも使用できます (たとえば、ページ 1 > ページ 2 > ページ 3 という要求の順序を保証します)。 ASP.NET Core MVC と Razor Pages のテンプレートのすべてのフォームで、偽造防止トークンが生成されます。 次の 2 つのビューの例では、偽造防止トークンが生成されます。

<form asp-action="Index" asp-controller="Home" method="post">
    <!-- ... -->
</form>

@using (Html.BeginForm("Index", "Home"))
{
    <!-- ... -->
}

タグ ヘルパーと HTML ヘルパー @Html.AntiForgeryToken を使用せず、偽造防止トークンを <form> 要素に明示的に追加します。

<form asp-action="Index" asp-controller="Home" method="post">
    @Html.AntiForgeryToken()

    <!-- ... -->
</form>

いずれの場合も、ASP.NET Core によって次の例のような非表示のフォーム フィールドが追加されます。

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">

ASP.NET Core には、偽造防止トークンを使用するための 3 つのフィルターが含まれています。

AddControllers を使用した偽造防止

AddControllers を呼び出しても、偽造防止トークンは有効に "なりません"。 組み込みの偽造防止トークンをサポートするには AddControllersWithViews を呼び出す必要があります。

複数のブラウザー タブと Synchronizer Token Pattern

Synchronizer Token Pattern を使う場合、最後に読み込まれたページにのみ、有効な偽造防止トークンが含まれています。 複数のタブを使うと、問題が発生することがあります。 たとえば、ユーザーが複数のタブを開いた場合:

  • 最後に読み込まれたタブにのみ、有効な偽造防止トークンが含まれています。
  • 以前に読み込まれたタブからの要求は、次のエラーで失敗します。Antiforgery token validation failed. The antiforgery cookie token and request token do not match

これで問題が発生する場合は、別の CSRF 保護パターンを検討してください。

AntiforgeryOptions を使用して偽造防止を構成する

AntiforgeryOptionsProgram.cs でカスタマイズします。

builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});

次の表に示すように、CookieBuilder クラスのプロパティを使って偽造防止の Cookie プロパティを設定します。

オプション 説明
Cookie 偽造防止 cookie の作成に使用される設定を決定します。
FormFieldName ビューで偽造防止トークンをレンダリングするために偽造防止システムによって使用される非表示フォーム フィールドの名前。
HeaderName 偽造防止システムによって使用されるヘッダーの名前。 null の場合、システムではフォーム データのみが考慮されます。
SuppressXFrameOptionsHeader X-Frame-Options ヘッダーの生成を抑制するかどうかを指定します。 既定では、ヘッダーは値 "SAMEORIGIN" を使用して生成されます。 既定値は false です。

詳細については、「CookieAuthenticationOptions」を参照してください。

IAntiforgery を使用して偽造防止トークンを生成する

IAntiforgery では、偽造防止機能を構成するための API が提供されます。 IAntiforgery を必須にするには、Program.csWebApplication.Services を使います。 次の例では、アプリのホーム ページからミドルウェアを使用して、偽造防止トークンを生成し、cookie として応答で送信します。

app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});

前の例では、XSRF-TOKEN という名前の cookie を設定しています。 クライアントはこの cookie を読み取り、その値を AJAX 要求にアタッチされたヘッダーとして提供できます。 たとえば、Angular には、既定で XSRF-TOKEN という名前の cookie を読み取る 組み込みの XSRF 保護が含まれています。

偽造防止の検証を要求する

ValidateAntiForgeryToken アクション フィルターは個々のアクションやコントローラーに対して、またはグローバルに適用できます。 このフィルターが適用されているアクションに対して行われた要求は、要求に有効な偽造防止トークンが含まれていない限り、ブロックされます。

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index()
{
    // ...

    return RedirectToAction();
}

ValidateAntiForgeryToken 属性には、HTTP GET 要求を含む、マークするアクション メソッドへの要求に対するトークンが必要です。 ValidateAntiForgeryToken 属性がアプリのコントローラー全体に適用されている場合は、IgnoreAntiforgeryToken 属性でオーバーライドできます。

安全でない HTTP メソッドについてのみ偽造防止トークンを自動的に検証する

ValidateAntiForgeryToken 属性を広範に適用してから IgnoreAntiforgeryToken 属性でオーバーライドするのではなく、AutoValidateAntiforgeryToken 属性を使用できます。 この属性は、ValidateAntiForgeryToken 属性と同じように動作しますが、次の HTTP メソッドを使用して行われた要求についてはトークンを要求しない点が異なります。

  • GET
  • HEAD
  • OPTIONS
  • TRACE

非 API のシナリオでは、AutoValidateAntiforgeryToken を広範に使うことをお勧めします。 この属性により、POST アクションが既定で保護されるようになります。 代わりの方法は、ValidateAntiForgeryToken が個々のアクション メソッドに適用されない限り、既定で偽造防止トークンを無視することです。 このシナリオでは、POST アクション メソッドが誤って保護されないままになり、アプリが CSRF 攻撃に対して脆弱になる可能性が高くなります。 すべての POST で、偽造防止トークンを送信する必要があります。

API には、トークンの cookie 以外の部分を送信するための自動メカニズムはありません。 実装は、クライアント コードの実装によって異なる場合があります。 いくつかの例を次に示します。

クラス レベルの例:

[AutoValidateAntiforgeryToken]
public class HomeController : Controller

グローバルな例:

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

グローバルまたはコントローラーの偽造防止属性をオーバーライドする

IgnoreAntiforgeryToken フィルターは、特定のアクション (またはコントローラー) に対して偽造防止トークンを不要にするために使用されます。 このフィルターを適用すると、より高いレベル (グローバルまたはコントローラー上) で指定されている ValidateAntiForgeryToken および AutoValidateAntiforgeryToken フィルターがオーバーライドされます。

[IgnoreAntiforgeryToken]
public IActionResult IndexOverride()
{
    // ...

    return RedirectToAction();
}

認証後にトークンを更新する

ユーザーが認証された後、ユーザーをビューまたは Razor Pages ページにリダイレクトすることによって、トークンを更新する必要があります。

JavaScript、AJAX、SPA

従来の HTML ベースのアプリでは、偽造防止トークンは非表示のフォーム フィールドを使用してサーバーに渡されます。 最新の JavaScript ベースのアプリと SPA では、多くの要求がプログラムによって行われます。 これらの AJAX 要求では、要求ヘッダーや cookie などの他の手法を使ってトークンを送信できます。

cookie が、認証トークンの格納と、サーバーでの API 要求の認証に使用されている場合、CSRF が問題になる可能性があります。 ローカル ストレージがトークンの格納に使われている場合は、ローカル ストレージの値はすべての要求でサーバーに自動的に送信されないため、CSRF の脆弱性が軽減される可能性があります。 ローカル ストレージを使用してクライアントに偽造防止トークンを格納し、要求ヘッダーとしてトークンを送信することをお勧めします。

Blazor

詳細については、「ASP.NET Core の Blazor 認証と認可」を参照してください。

JavaScript

JavaScript とビューを使うと、ビュー内からサービスを使ってトークンを作成できます。 ビューに IAntiforgery サービスを挿入し、GetAndStoreTokens を呼び出します。

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

@{
    ViewData["Title"] = "JavaScript";

    var requestToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
}

<input id="RequestVerificationToken" type="hidden" value="@requestToken" />

<button id="button" class="btn btn-primary">Submit with Token</button>
<div id="result" class="mt-2"></div>

@section Scripts {
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const resultElement = document.getElementById("result");

        document.getElementById("button").addEventListener("click", async () => {

            const response = await fetch("@Url.Action("FetchEndpoint")", {
                method: "POST",
                headers: {
                    RequestVerificationToken:
                        document.getElementById("RequestVerificationToken").value
                }
            });

            if (response.ok) {
                resultElement.innerText = await response.text();
            } else {
                resultElement.innerText = `Request Failed: ${response.status}`
            }
        });
    });
</script>
}

前の例では、JavaScript を使用して、AJAX POST ヘッダーの非表示フィールドの値を読み取っています。

この方法を使用すると、サーバーからの cookie の設定やクライアントからの読み取りを、直接処理する必要がなくなります。 ただし、IAntiforgery サービスを挿入できない場合は、JavaScript を使用して cookie のトークンにアクセスします。

  • サーバーへの追加要求でトークンにアクセスします (通常は same-origin)。
  • cookie のコンテンツを使用して、トークンの値を含むヘッダーを作成します。

スクリプトで、X-XSRF-TOKEN という名前の要求ヘッダーでトークンを送信することが想定されている場合は、X-XSRF-TOKEN ヘッダーを検索するように偽造防止サービスを構成します。

builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

次の例では、要求トークンを書き込む保護されたエンドポイントを、JavaScript で読み取り可能な cookie に追加します。

app.UseAuthorization();
app.MapGet("antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
{
    var tokens = forgeryService.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
            new CookieOptions { HttpOnly = false });

    return Results.Ok();
}).RequireAuthorization();

次の例では、JavaScript を使用して AJAX 要求を行い、トークンを取得して、適切なヘッダーを使って別の要求を行います。

var response = await fetch("/antiforgery/token", {
    method: "GET",
    headers: { "Authorization": authorizationToken }
});

if (response.ok) {
    // https://developer.mozilla.org/docs/web/api/document/cookie
    const xsrfToken = document.cookie
        .split("; ")
        .find(row => row.startsWith("XSRF-TOKEN="))
        .split("=")[1];

    response = await fetch("/JavaScript/FetchEndpoint", {
        method: "POST",
        headers: { "X-XSRF-TOKEN": xsrfToken, "Authorization": authorizationToken }
    });

    if (response.ok) {
        resultElement.innerText = await response.text();
    } else {
        resultElement.innerText = `Request Failed: ${response.status}`
    }
} else {    
    resultElement.innerText = `Request Failed: ${response.status}`
}

注意

要求ヘッダーとフォーム ペイロードの両方で偽造防止トークンが提供される場合、ヘッダー内のトークンのみが検証されます。

Minimal API を使用した偽造防止

AddAntiforgery および UseAntiforgery(IApplicationBuilder) を呼び出して、DI に偽造防止サービスを登録します。 偽造防止トークンは、クロスサイト 要求 フォージェリ攻撃 を軽減するために使用されます。

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", () => "Hello World!");

app.Run();

偽造防止ミドルウェア:

偽造防止トークンは、次の場合にのみ検証されます:

  • エンドポイントには、IAntiforgeryMetadata を実装する メタデータ RequiresValidation=true が含まれています。
  • エンドポイントに関連付けられている HTTP メソッドは、関連する HTTP メソッド です。 関連するメソッドは、TRACE、OPTIONS、HEAD、GET を除くすべての HTTP メソッド です。
  • 要求は有効なエンドポイントに関連付けられています。

注: 手動で有効にした場合、ユーザーが認証されていないときにフォーム データを読み取らないように、認証ミドルウェアと承認ミドルウェアの後に偽造防止ミドルウェアを実行する必要があります。

既定では、フォーム データを受け入れる最小限の API では、偽造防止トークンの検証が必要です。

次の GenerateForm メソッドを考えてみましょう:

public static string GenerateForm(string action, 
    AntiforgeryTokenSet token, bool UseToken=true)
{
    string tokenInput = "";
    if (UseToken)
    {
        tokenInput = $@"<input name=""{token.FormFieldName}""
                         type=""hidden"" value=""{token.RequestToken}"" />";
    }

    return $@"
    <html><body>
        <form action=""{action}"" method=""POST"" enctype=""multipart/form-data"">
            {tokenInput}
            <input type=""text"" name=""name"" />
            <input type=""date"" name=""dueDate"" />
            <input type=""checkbox"" name=""isCompleted"" />
            <input type=""submit"" />
        </form>
    </body></html>
";
}

上記のコードには、アクション、偽造防止トークン、および bool トークンを使用する必要があるかどうかを示す 3 つの引数があります。

次の例を考えてみましょう:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

// Pass token
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    return Results.Content(MyHtml.GenerateForm("/todo", token), "text/html");
});

// Don't pass a token, fails
app.MapGet("/SkipToken", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    return Results.Content(MyHtml.GenerateForm("/todo",token, false ), "text/html");
});

// Post to /todo2. DisableAntiforgery on that endpoint so no token needed.
app.MapGet("/DisableAntiforgery", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    return Results.Content(MyHtml.GenerateForm("/todo2", token, false), "text/html");
});

app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));

app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo))
                                                .DisableAntiforgery();

app.Run();

class Todo
{
    public required string Name { get; set; }
    public bool IsCompleted { get; set; }
    public DateTime DueDate { get; set; }
}

public static class MyHtml
{
    public static string GenerateForm(string action, 
        AntiforgeryTokenSet token, bool UseToken=true)
    {
        string tokenInput = "";
        if (UseToken)
        {
            tokenInput = $@"<input name=""{token.FormFieldName}""
                             type=""hidden"" value=""{token.RequestToken}"" />";
        }

        return $@"
        <html><body>
            <form action=""{action}"" method=""POST"" enctype=""multipart/form-data"">
                {tokenInput}
                <input type=""text"" name=""name"" />
                <input type=""date"" name=""dueDate"" />
                <input type=""checkbox"" name=""isCompleted"" />
                <input type=""submit"" />
            </form>
        </body></html>
    ";
    }
}

上記のコードでは、以下に投稿します:

  • /todo 有効な偽造防止トークンが必要です。
  • DisableAntiforgery が呼び出されるため、有効な偽造防止トークンは必要 /todo2 ありません
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));

app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo))
                                                .DisableAntiforgery();

次の投稿を行います:

  • /todo は、偽造防止トークンが有効であるため、/ エンドポイントによって生成されたフォームから成功します。
  • /todo は、偽造防止が /SkipToken 含まれていないため、失敗によって生成されたフォームから取得されます。
  • /todo2 は、偽造防止が必要ないため、/DisableAntiforgery エンドポイントによって生成されたフォームから正常に実行されます。
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));

app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo))
                                                .DisableAntiforgery();

フォームが有効な偽造防止トークンなしで送信された場合:

  • 開発環境では、例外がスローされます。
  • 運用環境では、メッセージがログに記録されます。

Windows 認証と偽造防止 cookie

Windows 認証を使用するときは、cookie の場合と同じ方法で、アプリケーション エンドポイントを CSRF 攻撃から保護する必要があります。 ブラウザーによって認証コンテキストがサーバーに暗黙的に送信されるため、エンドポイントを CSRF 攻撃から保護する必要があります。

偽造防止を強化する

IAntiforgeryAdditionalDataProvider 型を使用すると、開発者は、トークンごとに追加データをラウンドトリップすることで、CSRF 対策システムの動作を強化できます。 フィールド トークンを生成するたびに GetAdditionalData メソッドを呼び出し、戻り値を生成されるトークンに埋め込みます。 実装者は、タイムスタンプ、nonce、または他の値を返し、トークンの検証時に ValidateAdditionalData を呼び出してこのデータを検証できます。 クライアントのユーザー名は生成されたトークンに既に埋め込まれているので、この情報を含める必要はありません。 トークンに補足データが含まれていても、IAntiForgeryAdditionalDataProvider が構成されていない場合は、補足データは検証されません。

その他の技術情報

クロスサイト リクエスト フォージェリ (XSRF または CSRF とも呼ばれます) は、Web ホスト アプリに対する攻撃であり、悪意のある Web アプリがクライアント ブラウザーとそのブラウザーを信頼する Web アプリとの間の対話に、影響を与える可能性があります。 これらの攻撃は、Web ブラウザーが Web サイトへのすべての要求に対して何らかの種類の認証トークンを自動的に送信するために発生する可能性があります。 この形式の悪用は、攻撃でユーザーの以前に認証されたセッションが利用されるため、"ワンクリック攻撃" または "セッション ライディング" としても知られます。

CSRF 攻撃の例:

  1. ユーザーが、フォーム認証を使用して www.good-banking-site.example.com にサインインします。 サーバーはユーザーを認証し、認証 cookie を含む応答を発行します。 サイトは、有効な認証 cookie と共に受信したすべての要求を信頼するため、攻撃に対して脆弱です。

  2. ユーザーが悪意のあるサイト www.bad-crook-site.example.com にアクセスします。

    悪意のあるサイト www.bad-crook-site.example.com には、次の例のような HTML フォームが含まれています。

    <h1>Congratulations! You're a Winner!</h1>
    <form action="https://good-banking-site.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click to collect your prize!" />
    </form>
    

    悪意のあるサイトではなく、脆弱なサイトにフォームの action が POST されていることに注意してください。 これが CSRF の "クロスサイト" 部分です。

  3. ユーザーが送信ボタンを選びます。 ブラウザーは要求を行い、要求されたドメイン www.good-banking-site.example.com に対する認証 cookie を自動的に含めます。

  4. 要求はユーザーの認証コンテキストを使用して www.good-banking-site.example.com サーバー上で実行され、認証されたユーザーが実行を許可されている任意のアクションを実行できます。

ユーザーがボタンを選んでフォームを送信するシナリオに加えて、悪意のあるサイトでは次のこともできます。

  • フォームを自動的に送信するスクリプトを実行する。
  • フォーム送信を AJAX 要求として送信する。
  • CSS を使用してフォームを非表示にする。

これらの代替シナリオでは、最初に悪意のあるサイトにアクセスすること以外に、ユーザーによる操作や入力は必要ありません。

HTTPS を使用しても CSRF 攻撃を防ぐことはできません。 悪意のあるサイトは、安全ではない要求を送信するのと同じように簡単に、https://www.good-banking-site.com/ 要求を送信できます。

一部の攻撃は GET 要求に応答するエンドポイントを対象にしており、その場合、イメージ タグを使用してアクションを実行できます。 この形式の攻撃は、イメージは許可するが JavaScript はブロックするフォーラム サイトでよく使用されます。 GET 要求で状態を変更するアプリ (変数またはリソースが変更される場合) は、悪意のある攻撃に対して脆弱です。 状態を変更する GET 要求は安全ではありません。 ベスト プラクティスは、GET 要求で状態を変更しないことです。

次の理由により、認証に cookie を使用する Web アプリに対して、CSRF 攻撃が行われる可能性があります。

  • Web アプリによって発行された cookie がブラウザーで格納されます。
  • 格納される cookie には、認証されたユーザーのセッション cookie が含まれます。
  • アプリへの要求がブラウザー内で生成された方法に関係なく、ブラウザーからは、ドメインに関連付けられているすべての cookie が要求ごとに Web アプリに送信されます。

ただし、CSRF 攻撃は、cookie の悪用に限定されません。 たとえば、基本認証やダイジェスト認証も脆弱です。 ユーザーが基本認証またはダイジェスト認証を使用してサインインすると、セッションが終了するまで、ブラウザーによって資格情報が自動的に送信されます。

このコンテキストでの "セッション" とは、その間ユーザーが認証されている、クライアント側のセッションを指します。 サーバー側セッションや ASP.NET Core のセッション ミドルウェアとは無関係です。

ユーザーは、予防措置を講じて CSRF の脆弱性を防ぐことができます。

  • Web アプリを使い終わったら、それからサインアウトします。
  • ブラウザーの cookie を定期的にクリアします。

ただし、CSRF の脆弱性は、根本的に、エンド ユーザーではなく Web アプリに関する問題です。

認証の基礎

Cookie ベースの認証は、一般的な認証形式です。 トークン ベースの認証システムは普及が広がっています (特にシングルページ アプリケーション (SPA) の場合)。

ユーザーは、ユーザー名とパスワードを使って認証を行うと、認証と承認に使用できる認証チケットが含まれるトークンを発行されます。 トークンは、クライアントが行うすべての要求で送信される cookie として格納されます。 この cookie の生成と検証は、Cookie 認証ミドルウェアによって行われます。 ミドルウェアによって、ユーザー プリンシパルは暗号化された cookie にシリアル化されます。 後続の要求では、ミドルウェアによって cookie が検証され、プリンシパルが再作成されて、そのプリンシパルが HttpContext.User プロパティに割り当てられます。

トークンベースの認証

認証されたユーザーには、トークンが発行されます (偽造防止トークンではありません)。 このトークンには、クレームまたはアプリで保持されているユーザー状態をアプリに示す参照トークンの形式で、ユーザー情報が含まれます。 ユーザーが認証を必要とするリソースにアクセスしようとすると、ベアラー トークンの形式の追加承認ヘッダーと共にトークンがアプリに送信されます。 この方法により、アプリはステートレスになります。 後続の要求ごとに、サーバー側の検証のために要求でトークンが渡されます。 このトークンは、"暗号化" されておらず、"エンコード" されています。 サーバーでは、トークンをデコードして情報にアクセスします。 後続の要求でトークンを送信するため、トークンはブラウザーのローカル ストレージに格納されます。 トークンをブラウザーのローカル ストレージに配置して取得し、ベアラー トークンとして使用すると、CSRF 攻撃に対する保護が提供されます。 ただし、XSS または侵害された外部 javascript ファイルを介したスクリプト インジェクションに対してアプリが脆弱な場合、攻撃者はローカル ストレージから任意の値を取得して自分自身に送信する可能性があります。 ASP.NET Core では、既定ですべてのサーバー側出力が変数からエンコードされるため、XSS のリスクが軽減されます。 Html.Raw を使用するか、信頼されていない入力でカスタム コードを使用してこの動作をオーバーライドすると、XSS のリスクが高くなる可能性があります。

トークンがブラウザーのローカル ストレージに格納される場合は、CSRF の脆弱性について心配しないでください。 CSRF が問題になるのは、トークンが cookie に格納さるときです。 詳しくは、GitHub のイシュー「SPA code sample adds two cookies (SPA のコード サンプルで 2 つの cookie が追加される)」をご覧ください。

1 つのドメインでホストされている複数のアプリ

共有ホスティング環境は、セッション ハイジャック、ログイン CSRF、その他の攻撃に対して脆弱です。

example1.contoso.netexample2.contoso.net は異なるホストですが、*.contoso.net ドメインの下のホスト間には暗黙的な信頼関係があります。 この暗黙的な信頼関係により、信頼できないホストが互いの cookie に影響を与えることができる可能性があります (AJAX 要求を管理する同一オリジン ポリシーは、必ずしも HTTP cookie に適用されるとは限りません)。

同じドメインでホストされているアプリ間で信頼された cookie を悪用する攻撃は、ドメインを共有しないことで回避できます。 各アプリが独自のドメインでホストされている場合、悪用するための暗黙的な cookie の信頼関係はありません。

ASP.NET Core での偽造防止

警告

ASP.NET Core では、ASP.NET Core データ保護を使用して偽造防止が実装されています。 サーバー ファームで動作するように、データ保護スタックを構成する必要があります。 詳しくは、データ保護の構成に関する記事をご覧ください。

次のいずれかの API が Program.cs で呼び出されると、偽造防止ミドルウェアが依存関係挿入コンテナーに追加されます。

FormTagHelper により HTML フォーム要素に偽造防止トークンが挿入されます。 Razor ファイルの次のマークアップによって、偽造防止トークンが自動的に生成されます。

<form method="post">
    <!-- ... -->
</form>

同様に、フォームのメソッドが GET でない場合は、IHtmlHelper.BeginForm によって既定で偽造防止トークンが生成されます。

HTML フォーム要素に対する偽造防止トークンの自動生成は、<form> タグに method="post" 属性が含まれていて、次のいずれかが当てはまる場合に行われます。

  • action 属性が空です (action="")。
  • action 属性が指定されていません (<form method="post">)。

HTML フォーム要素に対する偽造防止トークンの自動生成は、無効にすることができます。

  • 偽造防止トークンを明示的に無効にするには、asp-antiforgery 属性を使います。

    <form method="post" asp-antiforgery="false">
        <!-- ... -->
    </form>
    
  • タグ ヘルパーの ! オプトアウト シンボルを使うと、フォーム要素がタグ ヘルパーからオプトアウトされます。

    <!form method="post">
        <!-- ... -->
    </!form>
    
  • ビューから FormTagHelper を削除します。 Razor ビューに次のディレクティブを追加することで、ビューから FormTagHelper を削除できます。

    @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
    

Note

Razor Pages は XSRF/CSRF から自動的に保護されます。 詳細しくは、「XSRF/CSRF および Razor ページ」をご覧ください。

CSRF 攻撃を防ぐ最も一般的な方法は、"シンクロナイザー トークン パターン" (STP) を使うことです。 STP は、ユーザーがフォーム データを含むページを要求するときに使われます。

  1. サーバーは、現在のユーザーの ID に関連付けられているトークンをクライアントに送信します。
  2. クライアントは、検証のためにトークンをサーバーに送り返します。
  3. サーバーが受け取ったトークンが、認証されたユーザーの ID と一致しない場合、要求は拒否されます。

トークンは一意で、予測できません。 トークンは、一連の要求の適切な順序を保証するためにも使用できます (たとえば、ページ 1 > ページ 2 > ページ 3 という要求の順序を保証します)。 ASP.NET Core MVC と Razor Pages のテンプレートのすべてのフォームで、偽造防止トークンが生成されます。 次の 2 つのビューの例では、偽造防止トークンが生成されます。

<form asp-action="Index" asp-controller="Home" method="post">
    <!-- ... -->
</form>

@using (Html.BeginForm("Index", "Home"))
{
    <!-- ... -->
}

タグ ヘルパーと HTML ヘルパー @Html.AntiForgeryToken を使用せず、偽造防止トークンを <form> 要素に明示的に追加します。

<form asp-action="Index" asp-controller="Home" method="post">
    @Html.AntiForgeryToken()

    <!-- ... -->
</form>

いずれの場合も、ASP.NET Core によって次の例のような非表示のフォーム フィールドが追加されます。

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">

ASP.NET Core には、偽造防止トークンを使用するための 3 つのフィルターが含まれています。

AddControllers を使用した偽造防止

AddControllers を呼び出しても、偽造防止トークンは有効に "なりません"。 組み込みの偽造防止トークンをサポートするには AddControllersWithViews を呼び出す必要があります。

複数のブラウザー タブと Synchronizer Token Pattern

Synchronizer Token Pattern を使う場合、最後に読み込まれたページにのみ、有効な偽造防止トークンが含まれています。 複数のタブを使うと、問題が発生することがあります。 たとえば、ユーザーが複数のタブを開いた場合:

  • 最後に読み込まれたタブにのみ、有効な偽造防止トークンが含まれています。
  • 以前に読み込まれたタブからの要求は、次のエラーで失敗します。Antiforgery token validation failed. The antiforgery cookie token and request token do not match

これで問題が発生する場合は、別の CSRF 保護パターンを検討してください。

AntiforgeryOptions を使用して偽造防止を構成する

AntiforgeryOptionsProgram.cs でカスタマイズします。

builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});

次の表に示すように、CookieBuilder クラスのプロパティを使って偽造防止の Cookie プロパティを設定します。

オプション 説明
Cookie 偽造防止 cookie の作成に使用される設定を決定します。
FormFieldName ビューで偽造防止トークンをレンダリングするために偽造防止システムによって使用される非表示フォーム フィールドの名前。
HeaderName 偽造防止システムによって使用されるヘッダーの名前。 null の場合、システムではフォーム データのみが考慮されます。
SuppressXFrameOptionsHeader X-Frame-Options ヘッダーの生成を抑制するかどうかを指定します。 既定では、ヘッダーは値 "SAMEORIGIN" を使用して生成されます。 既定値は false です。

詳細については、「CookieAuthenticationOptions」を参照してください。

IAntiforgery を使用して偽造防止トークンを生成する

IAntiforgery では、偽造防止機能を構成するための API が提供されます。 IAntiforgery を必須にするには、Program.csWebApplication.Services を使います。 次の例では、アプリのホーム ページからミドルウェアを使用して、偽造防止トークンを生成し、cookie として応答で送信します。

app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});

前の例では、XSRF-TOKEN という名前の cookie を設定しています。 クライアントはこの cookie を読み取り、その値を AJAX 要求にアタッチされたヘッダーとして提供できます。 たとえば、Angular には、既定で XSRF-TOKEN という名前の cookie を読み取る 組み込みの XSRF 保護が含まれています。

偽造防止の検証を要求する

ValidateAntiForgeryToken アクション フィルターは個々のアクションやコントローラーに対して、またはグローバルに適用できます。 このフィルターが適用されているアクションに対して行われた要求は、要求に有効な偽造防止トークンが含まれていない限り、ブロックされます。

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index()
{
    // ...

    return RedirectToAction();
}

ValidateAntiForgeryToken 属性には、HTTP GET 要求を含む、マークするアクション メソッドへの要求に対するトークンが必要です。 ValidateAntiForgeryToken 属性がアプリのコントローラー全体に適用されている場合は、IgnoreAntiforgeryToken 属性でオーバーライドできます。

安全でない HTTP メソッドについてのみ偽造防止トークンを自動的に検証する

ValidateAntiForgeryToken 属性を広範に適用してから IgnoreAntiforgeryToken 属性でオーバーライドするのではなく、AutoValidateAntiforgeryToken 属性を使用できます。 この属性は、ValidateAntiForgeryToken 属性と同じように動作しますが、次の HTTP メソッドを使用して行われた要求についてはトークンを要求しない点が異なります。

  • GET
  • HEAD
  • OPTIONS
  • TRACE

非 API のシナリオでは、AutoValidateAntiforgeryToken を広範に使うことをお勧めします。 この属性により、POST アクションが既定で保護されるようになります。 代わりの方法は、ValidateAntiForgeryToken が個々のアクション メソッドに適用されない限り、既定で偽造防止トークンを無視することです。 このシナリオでは、POST アクション メソッドが誤って保護されないままになり、アプリが CSRF 攻撃に対して脆弱になる可能性が高くなります。 すべての POST で、偽造防止トークンを送信する必要があります。

API には、トークンの cookie 以外の部分を送信するための自動メカニズムはありません。 実装は、クライアント コードの実装によって異なる場合があります。 いくつかの例を次に示します。

クラス レベルの例:

[AutoValidateAntiforgeryToken]
public class HomeController : Controller

グローバルな例:

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

グローバルまたはコントローラーの偽造防止属性をオーバーライドする

IgnoreAntiforgeryToken フィルターは、特定のアクション (またはコントローラー) に対して偽造防止トークンを不要にするために使用されます。 このフィルターを適用すると、より高いレベル (グローバルまたはコントローラー上) で指定されている ValidateAntiForgeryToken および AutoValidateAntiforgeryToken フィルターがオーバーライドされます。

[IgnoreAntiforgeryToken]
public IActionResult IndexOverride()
{
    // ...

    return RedirectToAction();
}

認証後にトークンを更新する

ユーザーが認証された後、ユーザーをビューまたは Razor Pages ページにリダイレクトすることによって、トークンを更新する必要があります。

JavaScript、AJAX、SPA

従来の HTML ベースのアプリでは、偽造防止トークンは非表示のフォーム フィールドを使用してサーバーに渡されます。 最新の JavaScript ベースのアプリと SPA では、多くの要求がプログラムによって行われます。 これらの AJAX 要求では、他の手法 (要求ヘッダーや cookie など) を使ってトークンを送信できます。

cookie が、認証トークンの格納と、サーバーでの API 要求の認証に使用されている場合、CSRF が問題になる可能性があります。 ローカル ストレージがトークンの格納に使われている場合は、ローカル ストレージの値はすべての要求でサーバーに自動的に送信されないため、CSRF の脆弱性が軽減される可能性があります。 ローカル ストレージを使用してクライアントに偽造防止トークンを格納し、要求ヘッダーとしてトークンを送信することをお勧めします。

JavaScript

JavaScript とビューを使うと、ビュー内からサービスを使ってトークンを作成できます。 ビューに IAntiforgery サービスを挿入し、GetAndStoreTokens を呼び出します。

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

@{
    ViewData["Title"] = "JavaScript";

    var requestToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
}

<input id="RequestVerificationToken" type="hidden" value="@requestToken" />

<button id="button" class="btn btn-primary">Submit with Token</button>
<div id="result" class="mt-2"></div>

@section Scripts {
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const resultElement = document.getElementById("result");

        document.getElementById("button").addEventListener("click", async () => {

            const response = await fetch("@Url.Action("FetchEndpoint")", {
                method: "POST",
                headers: {
                    RequestVerificationToken:
                        document.getElementById("RequestVerificationToken").value
                }
            });

            if (response.ok) {
                resultElement.innerText = await response.text();
            } else {
                resultElement.innerText = `Request Failed: ${response.status}`
            }
        });
    });
</script>
}

前の例では、JavaScript を使用して、AJAX POST ヘッダーの非表示フィールドの値を読み取っています。

この方法を使用すると、サーバーからの cookie の設定やクライアントからの読み取りを、直接処理する必要がなくなります。 ただし、IAntiforgery サービスを挿入できない場合は、JavaScript を使用して cookie のトークンにアクセスします。

  • サーバーへの追加要求でトークンにアクセスします (通常は same-origin)。
  • cookie のコンテンツを使用して、トークンの値を含むヘッダーを作成します。

スクリプトで、X-XSRF-TOKEN という名前の要求ヘッダーでトークンを送信することが想定されている場合は、X-XSRF-TOKEN ヘッダーを検索するように偽造防止サービスを構成します。

builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

次の例では、要求トークンを書き込む保護されたエンドポイントを、JavaScript で読み取り可能な cookie に追加します。

app.UseAuthorization();
app.MapGet("antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
{
    var tokens = forgeryService.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
            new CookieOptions { HttpOnly = false });

    return Results.Ok();
}).RequireAuthorization();

次の例では、JavaScript を使用して AJAX 要求を行い、トークンを取得して、適切なヘッダーを使って別の要求を行います。

var response = await fetch("/antiforgery/token", {
    method: "GET",
    headers: { "Authorization": authorizationToken }
});

if (response.ok) {
    // https://developer.mozilla.org/docs/web/api/document/cookie
    const xsrfToken = document.cookie
        .split("; ")
        .find(row => row.startsWith("XSRF-TOKEN="))
        .split("=")[1];

    response = await fetch("/JavaScript/FetchEndpoint", {
        method: "POST",
        headers: { "X-XSRF-TOKEN": xsrfToken, "Authorization": authorizationToken }
    });

    if (response.ok) {
        resultElement.innerText = await response.text();
    } else {
        resultElement.innerText = `Request Failed: ${response.status}`
    }
} else {    
    resultElement.innerText = `Request Failed: ${response.status}`
}

注意

要求ヘッダーとフォーム ペイロードの両方で偽造防止トークンが提供される場合、ヘッダー内のトークンのみが検証されます。

Minimal API を使用した偽造防止

Minimal APIs では含まれているフィルター (ValidateAntiForgeryTokenAutoValidateAntiforgeryTokenIgnoreAntiforgeryToken) の使用がサポートされていませんが、IAntiforgery により、要求を検証するために必要な API が提供されます。

次の例では、偽造防止トークンを検証するフィルターを作成します。

internal static class AntiForgeryExtensions
{
    public static TBuilder ValidateAntiforgery<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
    {
        return builder.AddEndpointFilter(routeHandlerFilter: async (context, next) =>
        {
            try
            {
                var antiForgeryService = context.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
                await antiForgeryService.ValidateRequestAsync(context.HttpContext);
            }
            catch (AntiforgeryValidationException)
            {
                return Results.BadRequest("Antiforgery token validation failed.");
            }

            return await next(context);

        });
    }
}

その後、そのフィルターをエンドポイントに適用できます。

app.MapPost("api/upload", (IFormFile name) => Results.Accepted())
    .RequireAuthorization()
    .ValidateAntiforgery();

Windows 認証と偽造防止 cookie

Windows 認証を使用するときは、cookie の場合と同じ方法で、アプリケーション エンドポイントを CSRF 攻撃から保護する必要があります。 ブラウザーによって認証コンテキストがサーバーに暗黙的に送信されるため、エンドポイントを CSRF 攻撃から保護する必要があります。

偽造防止を強化する

IAntiforgeryAdditionalDataProvider 型を使用すると、開発者は、トークンごとに追加データをラウンドトリップすることで、CSRF 対策システムの動作を強化できます。 フィールド トークンを生成するたびに GetAdditionalData メソッドを呼び出し、戻り値を生成されるトークンに埋め込みます。 実装者は、タイムスタンプ、nonce、または他の値を返し、トークンの検証時に ValidateAdditionalData を呼び出してこのデータを検証できます。 クライアントのユーザー名は生成されたトークンに既に埋め込まれているので、この情報を含める必要はありません。 トークンに補足データが含まれていても、IAntiForgeryAdditionalDataProvider が構成されていない場合は、補足データは検証されません。

その他の技術情報

クロスサイト リクエスト フォージェリ (XSRF または CSRF とも呼ばれます) は、Web ホスト アプリに対する攻撃であり、悪意のある Web アプリがクライアント ブラウザーとそのブラウザーを信頼する Web アプリとの間の対話に、影響を与える可能性があります。 これらの攻撃は、Web ブラウザーが Web サイトへのすべての要求に対して何らかの種類の認証トークンを自動的に送信するために発生する可能性があります。 この形式の悪用は、攻撃でユーザーの以前に認証されたセッションが利用されるため、"ワンクリック攻撃" または "セッション ライディング" としても知られます。

CSRF 攻撃の例:

  1. ユーザーが、フォーム認証を使用して www.good-banking-site.example.com にサインインします。 サーバーはユーザーを認証し、認証 cookie を含む応答を発行します。 サイトは、有効な認証 cookie と共に受信したすべての要求を信頼するため、攻撃に対して脆弱です。

  2. ユーザーが悪意のあるサイト www.bad-crook-site.example.com にアクセスします。

    悪意のあるサイト www.bad-crook-site.example.com には、次の例のような HTML フォームが含まれています。

    <h1>Congratulations! You're a Winner!</h1>
    <form action="https://good-banking-site.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click to collect your prize!" />
    </form>
    

    悪意のあるサイトではなく、脆弱なサイトにフォームの action が POST されていることに注意してください。 これが CSRF の "クロスサイト" 部分です。

  3. ユーザーが送信ボタンを選びます。 ブラウザーは要求を行い、要求されたドメイン www.good-banking-site.example.com に対する認証 cookie を自動的に含めます。

  4. 要求はユーザーの認証コンテキストを使用して www.good-banking-site.example.com サーバー上で実行され、認証されたユーザーが実行を許可されている任意のアクションを実行できます。

ユーザーがボタンを選んでフォームを送信するシナリオに加えて、悪意のあるサイトでは次のこともできます。

  • フォームを自動的に送信するスクリプトを実行する。
  • フォーム送信を AJAX 要求として送信する。
  • CSS を使用してフォームを非表示にする。

これらの代替シナリオでは、最初に悪意のあるサイトにアクセスすること以外に、ユーザーによる操作や入力は必要ありません。

HTTPS を使用しても CSRF 攻撃を防ぐことはできません。 悪意のあるサイトは、安全ではない要求を送信するのと同じように簡単に、https://www.good-banking-site.com/ 要求を送信できます。

一部の攻撃は GET 要求に応答するエンドポイントを対象にしており、その場合、イメージ タグを使用してアクションを実行できます。 この形式の攻撃は、イメージは許可するが JavaScript はブロックするフォーラム サイトでよく使用されます。 GET 要求で状態を変更するアプリ (変数またはリソースが変更される場合) は、悪意のある攻撃に対して脆弱です。 状態を変更する GET 要求は安全ではありません。 ベスト プラクティスは、GET 要求で状態を変更しないことです。

次の理由により、認証に cookie を使用する Web アプリに対して、CSRF 攻撃が行われる可能性があります。

  • Web アプリによって発行された cookie がブラウザーで格納されます。
  • 格納される cookie には、認証されたユーザーのセッション cookie が含まれます。
  • アプリへの要求がブラウザー内で生成された方法に関係なく、ブラウザーからは、ドメインに関連付けられているすべての cookie が要求ごとに Web アプリに送信されます。

ただし、CSRF 攻撃は、cookie の悪用に限定されません。 たとえば、基本認証やダイジェスト認証も脆弱です。 ユーザーが基本認証またはダイジェスト認証を使用してサインインすると、セッションが終了するまで、ブラウザーによって資格情報が自動的に送信されます。

このコンテキストでの "セッション" とは、その間ユーザーが認証されている、クライアント側のセッションを指します。 サーバー側セッションや ASP.NET Core のセッション ミドルウェアとは無関係です。

ユーザーは、予防措置を講じて CSRF の脆弱性を防ぐことができます。

  • Web アプリを使い終わったら、それからサインアウトします。
  • ブラウザーの cookie を定期的にクリアします。

ただし、CSRF の脆弱性は、根本的に、エンド ユーザーではなく Web アプリに関する問題です。

認証の基礎

Cookie ベースの認証は、一般的な認証形式です。 トークン ベースの認証システムは普及が広がっています (特にシングルページ アプリケーション (SPA) の場合)。

ユーザーは、ユーザー名とパスワードを使って認証を行うと、認証と承認に使用できる認証チケットが含まれるトークンを発行されます。 トークンは、クライアントが行うすべての要求で送信される cookie として格納されます。 この cookie の生成と検証は、Cookie 認証ミドルウェアによって行われます。 ミドルウェアによって、ユーザー プリンシパルは暗号化された cookie にシリアル化されます。 後続の要求では、ミドルウェアによって cookie が検証され、プリンシパルが再作成されて、そのプリンシパルが HttpContext.User プロパティに割り当てられます。

トークンベースの認証

認証されたユーザーには、トークンが発行されます (偽造防止トークンではありません)。 このトークンには、クレームまたはアプリで保持されているユーザー状態をアプリに示す参照トークンの形式で、ユーザー情報が含まれます。 ユーザーが認証を必要とするリソースにアクセスしようとすると、ベアラー トークンの形式の追加承認ヘッダーと共にトークンがアプリに送信されます。 この方法により、アプリはステートレスになります。 後続の要求ごとに、サーバー側の検証のために要求でトークンが渡されます。 このトークンは、"暗号化" されておらず、"エンコード" されています。 サーバーでは、トークンをデコードして情報にアクセスします。 後続の要求でトークンを送信するため、トークンはブラウザーのローカル ストレージに格納されます。 トークンがブラウザーのローカル ストレージに格納される場合は、CSRF の脆弱性について心配しないでください。 CSRF が問題になるのは、トークンが cookie に格納さるときです。 詳しくは、GitHub のイシュー「SPA code sample adds two cookies (SPA のコード サンプルで 2 つの cookie が追加される)」をご覧ください。

1 つのドメインでホストされている複数のアプリ

共有ホスティング環境は、セッション ハイジャック、ログイン CSRF、その他の攻撃に対して脆弱です。

example1.contoso.netexample2.contoso.net は異なるホストですが、*.contoso.net ドメインの下のホスト間には暗黙的な信頼関係があります。 この暗黙的な信頼関係により、信頼できないホストが互いの cookie に影響を与えることができる可能性があります (AJAX 要求を管理する同一オリジン ポリシーは、必ずしも HTTP cookie に適用されるとは限りません)。

同じドメインでホストされているアプリ間で信頼された cookie を悪用する攻撃は、ドメインを共有しないことで回避できます。 各アプリが独自のドメインでホストされている場合、悪用するための暗黙的な cookie の信頼関係はありません。

ASP.NET Core での偽造防止

警告

ASP.NET Core では、ASP.NET Core データ保護を使用して偽造防止が実装されています。 サーバー ファームで動作するように、データ保護スタックを構成する必要があります。 詳しくは、データ保護の構成に関する記事をご覧ください。

次のいずれかの API が Program.cs で呼び出されると、偽造防止ミドルウェアが依存関係挿入コンテナーに追加されます。

FormTagHelper により HTML フォーム要素に偽造防止トークンが挿入されます。 Razor ファイルの次のマークアップによって、偽造防止トークンが自動的に生成されます。

<form method="post">
    <!-- ... -->
</form>

同様に、フォームのメソッドが GET でない場合は、IHtmlHelper.BeginForm によって既定で偽造防止トークンが生成されます。

HTML フォーム要素に対する偽造防止トークンの自動生成は、<form> タグに method="post" 属性が含まれていて、次のいずれかが当てはまる場合に行われます。

  • action 属性が空です (action="")。
  • action 属性が指定されていません (<form method="post">)。

HTML フォーム要素に対する偽造防止トークンの自動生成は、無効にすることができます。

  • 偽造防止トークンを明示的に無効にするには、asp-antiforgery 属性を使います。

    <form method="post" asp-antiforgery="false">
        <!-- ... -->
    </form>
    
  • タグ ヘルパーの ! オプトアウト シンボルを使うと、フォーム要素がタグ ヘルパーからオプトアウトされます。

    <!form method="post">
        <!-- ... -->
    </!form>
    
  • ビューから FormTagHelper を削除します。 Razor ビューに次のディレクティブを追加することで、ビューから FormTagHelper を削除できます。

    @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
    

Note

Razor Pages は XSRF/CSRF から自動的に保護されます。 詳細しくは、「XSRF/CSRF および Razor ページ」をご覧ください。

CSRF 攻撃を防ぐ最も一般的な方法は、"シンクロナイザー トークン パターン" (STP) を使うことです。 STP は、ユーザーがフォーム データを含むページを要求するときに使われます。

  1. サーバーは、現在のユーザーの ID に関連付けられているトークンをクライアントに送信します。
  2. クライアントは、検証のためにトークンをサーバーに送り返します。
  3. サーバーが受け取ったトークンが、認証されたユーザーの ID と一致しない場合、要求は拒否されます。

トークンは一意で、予測できません。 トークンは、一連の要求の適切な順序を保証するためにも使用できます (たとえば、ページ 1 > ページ 2 > ページ 3 という要求の順序を保証します)。 ASP.NET Core MVC と Razor Pages のテンプレートのすべてのフォームで、偽造防止トークンが生成されます。 次の 2 つのビューの例では、偽造防止トークンが生成されます。

<form asp-action="Index" asp-controller="Home" method="post">
    <!-- ... -->
</form>

@using (Html.BeginForm("Index", "Home"))
{
    <!-- ... -->
}

タグ ヘルパーと HTML ヘルパー @Html.AntiForgeryToken を使用せず、偽造防止トークンを <form> 要素に明示的に追加します。

<form asp-action="Index" asp-controller="Home" method="post">
    @Html.AntiForgeryToken()

    <!-- ... -->
</form>

いずれの場合も、ASP.NET Core によって次の例のような非表示のフォーム フィールドが追加されます。

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">

ASP.NET Core には、偽造防止トークンを使用するための 3 つのフィルターが含まれています。

AddControllers を使用した偽造防止

AddControllers を呼び出しても、偽造防止トークンは有効に "なりません"。 組み込みの偽造防止トークンをサポートするには AddControllersWithViews を呼び出す必要があります。

複数のブラウザー タブと Synchronizer Token Pattern

Synchronizer Token Pattern を使う場合、最後に読み込まれたページにのみ、有効な偽造防止トークンが含まれています。 複数のタブを使うと、問題が発生することがあります。 たとえば、ユーザーが複数のタブを開いた場合:

  • 最後に読み込まれたタブにのみ、有効な偽造防止トークンが含まれています。
  • 以前に読み込まれたタブからの要求は、次のエラーで失敗します。Antiforgery token validation failed. The antiforgery cookie token and request token do not match

これで問題が発生する場合は、別の CSRF 保護パターンを検討してください。

AntiforgeryOptions を使用して偽造防止を構成する

AntiforgeryOptionsProgram.cs でカスタマイズします。

builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});

次の表に示すように、CookieBuilder クラスのプロパティを使って偽造防止の Cookie プロパティを設定します。

オプション 説明
Cookie 偽造防止 cookie の作成に使用される設定を決定します。
FormFieldName ビューで偽造防止トークンをレンダリングするために偽造防止システムによって使用される非表示フォーム フィールドの名前。
HeaderName 偽造防止システムによって使用されるヘッダーの名前。 null の場合、システムではフォーム データのみが考慮されます。
SuppressXFrameOptionsHeader X-Frame-Options ヘッダーの生成を抑制するかどうかを指定します。 既定では、ヘッダーは値 "SAMEORIGIN" を使用して生成されます。 既定値は false です。

詳細については、「CookieAuthenticationOptions」を参照してください。

IAntiforgery を使用して偽造防止トークンを生成する

IAntiforgery では、偽造防止機能を構成するための API が提供されます。 IAntiforgery を必須にするには、Program.csWebApplication.Services を使います。 次の例では、アプリのホーム ページからミドルウェアを使用して、偽造防止トークンを生成し、cookie として応答で送信します。

app.UseRouting();

app.UseAuthorization();

var antiforgery = app.Services.GetRequiredService<IAntiforgery>();

app.Use((context, next) =>
{
    var requestPath = context.Request.Path.Value;

    if (string.Equals(requestPath, "/", StringComparison.OrdinalIgnoreCase)
        || string.Equals(requestPath, "/index.html", StringComparison.OrdinalIgnoreCase))
    {
        var tokenSet = antiforgery.GetAndStoreTokens(context);
        context.Response.Cookies.Append("XSRF-TOKEN", tokenSet.RequestToken!,
            new CookieOptions { HttpOnly = false });
    }

    return next(context);
});

前の例では、XSRF-TOKEN という名前の cookie を設定しています。 クライアントはこの cookie を読み取り、その値を AJAX 要求にアタッチされたヘッダーとして提供できます。 たとえば、Angular には、既定で XSRF-TOKEN という名前の cookie を読み取る 組み込みの XSRF 保護が含まれています。

偽造防止の検証を要求する

ValidateAntiForgeryToken アクション フィルターは個々のアクションやコントローラーに対して、またはグローバルに適用できます。 このフィルターが適用されているアクションに対して行われた要求は、要求に有効な偽造防止トークンが含まれていない限り、ブロックされます。

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index()
{
    // ...

    return RedirectToAction();
}

ValidateAntiForgeryToken 属性には、HTTP GET 要求を含む、マークするアクション メソッドへの要求に対するトークンが必要です。 ValidateAntiForgeryToken 属性がアプリのコントローラー全体に適用されている場合は、IgnoreAntiforgeryToken 属性でオーバーライドできます。

安全でない HTTP メソッドについてのみ偽造防止トークンを自動的に検証する

ValidateAntiForgeryToken 属性を広範に適用してから IgnoreAntiforgeryToken 属性でオーバーライドするのではなく、AutoValidateAntiforgeryToken 属性を使用できます。 この属性は、ValidateAntiForgeryToken 属性と同じように動作しますが、次の HTTP メソッドを使用して行われた要求についてはトークンを要求しない点が異なります。

  • GET
  • HEAD
  • OPTIONS
  • TRACE

非 API のシナリオでは、AutoValidateAntiforgeryToken を広範に使うことをお勧めします。 この属性により、POST アクションが既定で保護されるようになります。 代わりの方法は、ValidateAntiForgeryToken が個々のアクション メソッドに適用されない限り、既定で偽造防止トークンを無視することです。 このシナリオでは、POST アクション メソッドが誤って保護されないままになり、アプリが CSRF 攻撃に対して脆弱になる可能性が高くなります。 すべての POST で、偽造防止トークンを送信する必要があります。

API には、トークンの cookie 以外の部分を送信するための自動メカニズムはありません。 実装は、クライアント コードの実装によって異なる場合があります。 いくつかの例を次に示します。

クラス レベルの例:

[AutoValidateAntiforgeryToken]
public class HomeController : Controller

グローバルな例:

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

グローバルまたはコントローラーの偽造防止属性をオーバーライドする

IgnoreAntiforgeryToken フィルターは、特定のアクション (またはコントローラー) に対して偽造防止トークンを不要にするために使用されます。 このフィルターを適用すると、より高いレベル (グローバルまたはコントローラー上) で指定されている ValidateAntiForgeryToken および AutoValidateAntiforgeryToken フィルターがオーバーライドされます。

[IgnoreAntiforgeryToken]
public IActionResult IndexOverride()
{
    // ...

    return RedirectToAction();
}

認証後にトークンを更新する

ユーザーが認証された後、ユーザーをビューまたは Razor Pages ページにリダイレクトすることによって、トークンを更新する必要があります。

JavaScript、AJAX、SPA

従来の HTML ベースのアプリでは、偽造防止トークンは非表示のフォーム フィールドを使用してサーバーに渡されます。 最新の JavaScript ベースのアプリと SPA では、多くの要求がプログラムによって行われます。 これらの AJAX 要求では、他の手法 (要求ヘッダーや cookie など) を使ってトークンを送信できます。

cookie が、認証トークンの格納と、サーバーでの API 要求の認証に使用されている場合、CSRF が問題になる可能性があります。 ローカル ストレージがトークンの格納に使われている場合は、ローカル ストレージの値はすべての要求でサーバーに自動的に送信されないため、CSRF の脆弱性が軽減される可能性があります。 ローカル ストレージを使用してクライアントに偽造防止トークンを格納し、要求ヘッダーとしてトークンを送信することをお勧めします。

JavaScript

JavaScript とビューを使うと、ビュー内からサービスを使ってトークンを作成できます。 ビューに IAntiforgery サービスを挿入し、GetAndStoreTokens を呼び出します。

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

@{
    ViewData["Title"] = "JavaScript";

    var requestToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
}

<input id="RequestVerificationToken" type="hidden" value="@requestToken" />

<button id="button" class="btn btn-primary">Submit with Token</button>
<div id="result" class="mt-2"></div>

@section Scripts {
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const resultElement = document.getElementById("result");

        document.getElementById("button").addEventListener("click", async () => {

            const response = await fetch("@Url.Action("FetchEndpoint")", {
                method: "POST",
                headers: {
                    RequestVerificationToken:
                        document.getElementById("RequestVerificationToken").value
                }
            });

            if (response.ok) {
                resultElement.innerText = await response.text();
            } else {
                resultElement.innerText = `Request Failed: ${response.status}`
            }
        });
    });
</script>
}

前の例では、JavaScript を使用して、AJAX POST ヘッダーの非表示フィールドの値を読み取っています。

この方法を使用すると、サーバーからの cookie の設定やクライアントからの読み取りを、直接処理する必要がなくなります。 ただし、IAntiforgery サービスを挿入できない場合は、JavaScript を使って、サーバーへの追加要求から取得される (通常 same-origin) cookie のトークンにアクセスし、cookie の内容を使ってトークンの値を含むヘッダーを作成することもできます。

スクリプトで、X-XSRF-TOKEN という名前の要求ヘッダーでトークンを送信することが想定されている場合は、X-XSRF-TOKEN ヘッダーを検索するように偽造防止サービスを構成します。

builder.Services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

次の例では、要求トークンを書き込む保護されたエンドポイントを、JavaScript で読み取り可能な cookie に追加します。

app.UseAuthorization();
app.MapGet("antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
{
    var tokens = forgeryService.GetAndStoreTokens(context);
    context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!,
            new CookieOptions { HttpOnly = false });

    return Results.Ok();
}).RequireAuthorization();

次の例では、JavaScript を使用して AJAX 要求を行い、トークンを取得して、適切なヘッダーを使って別の要求を行います。

var response = await fetch("/antiforgery/token", {
    method: "GET",
    headers: { "Authorization": authorizationToken }
});

if (response.ok) {
    // https://developer.mozilla.org/docs/web/api/document/cookie
    const xsrfToken = document.cookie
        .split("; ")
        .find(row => row.startsWith("XSRF-TOKEN="))
        .split("=")[1];

    response = await fetch("/JavaScript/FetchEndpoint", {
        method: "POST",
        headers: { "X-XSRF-TOKEN": xsrfToken, "Authorization": authorizationToken }
    });

    if (response.ok) {
        resultElement.innerText = await response.text();
    } else {
        resultElement.innerText = `Request Failed: ${response.status}`
    }
} else {    
    resultElement.innerText = `Request Failed: ${response.status}`
}

Windows 認証と偽造防止 cookie

Windows 認証を使用するときは、cookie の場合と同じ方法で、アプリケーション エンドポイントを CSRF 攻撃から保護する必要があります。 ブラウザーによって認証コンテキストがサーバーに暗黙的に送信されるため、エンドポイントを CSRF 攻撃から保護する必要があります。

偽造防止を強化する

IAntiforgeryAdditionalDataProvider 型を使用すると、開発者は、トークンごとに追加データをラウンドトリップすることで、CSRF 対策システムの動作を強化できます。 フィールド トークンを生成するたびに GetAdditionalData メソッドを呼び出し、戻り値を生成されるトークンに埋め込みます。 実装者は、タイムスタンプ、nonce、または他の値を返し、トークンの検証時に ValidateAdditionalData を呼び出してこのデータを検証できます。 クライアントのユーザー名は生成されたトークンに既に埋め込まれているので、この情報を含める必要はありません。 トークンに補足データが含まれていても、IAntiForgeryAdditionalDataProvider が構成されていない場合は、補足データは検証されません。

その他の技術情報

クロスサイト リクエスト フォージェリ (XSRF または CSRF とも呼ばれます) は、Web ホスト アプリに対する攻撃であり、悪意のある Web アプリがクライアント ブラウザーとそのブラウザーを信頼する Web アプリとの間の対話に、影響を与える可能性があります。 これらの攻撃は、Web ブラウザーが Web サイトへのすべての要求に対して何らかの種類の認証トークンを自動的に送信するために発生する可能性があります。 この形式の悪用は、攻撃でユーザーの以前に認証されたセッションが利用されるため、"ワンクリック攻撃" または "セッション ライディング" としても知られます。

CSRF 攻撃の例:

  1. ユーザーが、フォーム認証を使用して www.good-banking-site.example.com にサインインします。 サーバーはユーザーを認証し、認証 cookie を含む応答を発行します。 サイトは、有効な認証 cookie と共に受信したすべての要求を信頼するため、攻撃に対して脆弱です。

  2. ユーザーが悪意のあるサイト www.bad-crook-site.example.com にアクセスします。

    悪意のあるサイト www.bad-crook-site.example.com には、次の例のような HTML フォームが含まれています。

    <h1>Congratulations! You're a Winner!</h1>
    <form action="https://good-banking-site.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click to collect your prize!" />
    </form>
    

    悪意のあるサイトではなく、脆弱なサイトにフォームの action が POST されていることに注意してください。 これが CSRF の "クロスサイト" 部分です。

  3. ユーザーが送信ボタンを選びます。 ブラウザーは要求を行い、要求されたドメイン www.good-banking-site.example.com に対する認証 cookie を自動的に含めます。

  4. 要求はユーザーの認証コンテキストを使用して www.good-banking-site.example.com サーバー上で実行され、認証されたユーザーが実行を許可されている任意のアクションを実行できます。

ユーザーがボタンを選んでフォームを送信するシナリオに加えて、悪意のあるサイトでは次のこともできます。

  • フォームを自動的に送信するスクリプトを実行する。
  • フォーム送信を AJAX 要求として送信する。
  • CSS を使用してフォームを非表示にする。

これらの代替シナリオでは、最初に悪意のあるサイトにアクセスすること以外に、ユーザーによる操作や入力は必要ありません。

HTTPS を使用しても CSRF 攻撃を防ぐことはできません。 悪意のあるサイトは、安全ではない要求を送信するのと同じように簡単に、https://www.good-banking-site.com/ 要求を送信できます。

一部の攻撃は GET 要求に応答するエンドポイントを対象にしており、その場合、イメージ タグを使用してアクションを実行できます。 この形式の攻撃は、イメージは許可するが JavaScript はブロックするフォーラム サイトでよく使用されます。 GET 要求で状態を変更するアプリ (変数またはリソースが変更される場合) は、悪意のある攻撃に対して脆弱です。 状態を変更する GET 要求は安全ではありません。 ベスト プラクティスは、GET 要求で状態を変更しないことです。

次の理由により、認証に cookie を使用する Web アプリに対して、CSRF 攻撃が行われる可能性があります。

  • Web アプリによって発行された cookie がブラウザーで格納されます。
  • 格納される cookie には、認証されたユーザーのセッション cookie が含まれます。
  • アプリへの要求がブラウザー内で生成された方法に関係なく、ブラウザーからは、ドメインに関連付けられているすべての cookie が要求ごとに Web アプリに送信されます。

ただし、CSRF 攻撃は、cookie の悪用に限定されません。 たとえば、基本認証やダイジェスト認証も脆弱です。 ユーザーが基本認証またはダイジェスト認証を使用してサインインすると、セッションが終了するまで、ブラウザーによって資格情報が自動的に送信されます。

このコンテキストでの "セッション" とは、その間ユーザーが認証されている、クライアント側のセッションを指します。 サーバー側セッションや ASP.NET Core のセッション ミドルウェアとは無関係です。

ユーザーは、予防措置を講じて CSRF の脆弱性を防ぐことができます。

  • Web アプリを使い終わったら、それからサインアウトします。
  • ブラウザーの cookie を定期的にクリアします。

ただし、CSRF の脆弱性は、根本的に、エンド ユーザーではなく Web アプリに関する問題です。

認証の基礎

Cookie ベースの認証は、一般的な認証形式です。 トークン ベースの認証システムは普及が広がっています (特にシングルページ アプリケーション (SPA) の場合)。

ユーザーは、ユーザー名とパスワードを使って認証を行うと、認証と承認に使用できる認証チケットが含まれるトークンを発行されます。 トークンは、クライアントが行うすべての要求で送信される cookie として格納されます。 この cookie の生成と検証は、Cookie 認証ミドルウェアによって行われます。 ミドルウェアによって、ユーザー プリンシパルは暗号化された cookie にシリアル化されます。 後続の要求では、ミドルウェアによって cookie が検証され、プリンシパルが再作成されて、そのプリンシパルが HttpContext.User プロパティに割り当てられます。

トークンベースの認証

認証されたユーザーには、トークンが発行されます (偽造防止トークンではありません)。 このトークンには、クレームまたはアプリで保持されているユーザー状態をアプリに示す参照トークンの形式で、ユーザー情報が含まれます。 ユーザーが認証を必要とするリソースにアクセスしようとすると、ベアラー トークンの形式の追加承認ヘッダーと共にトークンがアプリに送信されます。 この方法により、アプリはステートレスになります。 後続の要求ごとに、サーバー側の検証のために要求でトークンが渡されます。 このトークンは、"暗号化" されておらず、"エンコード" されています。 サーバーでは、トークンをデコードして情報にアクセスします。 後続の要求でトークンを送信するため、トークンはブラウザーのローカル ストレージに格納されます。 トークンがブラウザーのローカル ストレージに格納される場合は、CSRF の脆弱性について心配しないでください。 CSRF が問題になるのは、トークンが cookie に格納さるときです。 詳しくは、GitHub のイシュー「SPA code sample adds two cookies (SPA のコード サンプルで 2 つの cookie が追加される)」をご覧ください。

1 つのドメインでホストされている複数のアプリ

共有ホスティング環境は、セッション ハイジャック、ログイン CSRF、その他の攻撃に対して脆弱です。

example1.contoso.netexample2.contoso.net は異なるホストですが、*.contoso.net ドメインの下のホスト間には暗黙的な信頼関係があります。 この暗黙的な信頼関係により、信頼できないホストが互いの cookie に影響を与えることができる可能性があります (AJAX 要求を管理する同一オリジン ポリシーは、必ずしも HTTP cookie に適用されるとは限りません)。

同じドメインでホストされているアプリ間で信頼された cookie を悪用する攻撃は、ドメインを共有しないことで回避できます。 各アプリが独自のドメインでホストされている場合、悪用するための暗黙的な cookie の信頼関係はありません。

ASP.NET Core の偽造防止の構成

警告

ASP.NET Core では、ASP.NET Core データ保護を使用して偽造防止が実装されています。 サーバー ファームで動作するように、データ保護スタックを構成する必要があります。 詳しくは、データ保護の構成に関する記事をご覧ください。

次のいずれかの API が Startup.ConfigureServices で呼び出されると、偽造防止ミドルウェアが依存関係挿入コンテナーに追加されます。

ASP.NET Core 2.0 以降では、FormTagHelper により HTML フォーム要素に偽造防止トークンが挿入されます。 Razor ファイルの次のマークアップによって、偽造防止トークンが自動的に生成されます。

<form method="post">
    ...
</form>

同様に、フォームのメソッドが GET でない場合は、IHtmlHelper.BeginForm によって既定で偽造防止トークンが生成されます。

HTML フォーム要素に対する偽造防止トークンの自動生成は、<form> タグに method="post" 属性が含まれていて、次のいずれかが当てはまる場合に行われます。

  • action 属性が空です (action="")。
  • action 属性が指定されていません (<form method="post">)。

HTML フォーム要素に対する偽造防止トークンの自動生成は、無効にすることができます。

  • 偽造防止トークンを明示的に無効にするには、asp-antiforgery 属性を使います。

    <form method="post" asp-antiforgery="false">
        ...
    </form>
    
  • タグ ヘルパーの ! オプトアウト シンボルを使うと、フォーム要素がタグ ヘルパーからオプトアウトされます。

    <!form method="post">
        ...
    </!form>
    
  • ビューから FormTagHelper を削除します。 Razor ビューに次のディレクティブを追加することで、ビューから FormTagHelper を削除できます。

    @removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.FormTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers
    

Note

Razor Pages は XSRF/CSRF から自動的に保護されます。 詳細しくは、「XSRF/CSRF および Razor ページ」をご覧ください。

CSRF 攻撃を防ぐ最も一般的な方法は、"シンクロナイザー トークン パターン" (STP) を使うことです。 STP は、ユーザーがフォーム データを含むページを要求するときに使われます。

  1. サーバーは、現在のユーザーの ID に関連付けられているトークンをクライアントに送信します。
  2. クライアントは、検証のためにトークンをサーバーに送り返します。
  3. サーバーが受け取ったトークンが、認証されたユーザーの ID と一致しない場合、要求は拒否されます。

トークンは一意で、予測できません。 トークンは、一連の要求の適切な順序を保証するためにも使用できます (たとえば、ページ 1 > ページ 2 > ページ 3 という要求の順序を保証します)。 ASP.NET Core MVC と Razor Pages のテンプレートのすべてのフォームで、偽造防止トークンが生成されます。 次の 2 つのビューの例では、偽造防止トークンが生成されます。

<form asp-controller="Todo" asp-action="Create" method="post">
    ...
</form>

@using (Html.BeginForm("Create", "Todo"))
{
    ...
}

タグ ヘルパーと HTML ヘルパー @Html.AntiForgeryToken を使用せず、偽造防止トークンを <form> 要素に明示的に追加します。

<form action="/" method="post">
    @Html.AntiForgeryToken()
</form>

いずれの場合も、ASP.NET Core によって次の例のような非表示のフォーム フィールドが追加されます。

<input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkS ... s2-m9Yw">

ASP.NET Core には、偽造防止トークンを使用するための 3 つのフィルターが含まれています。

偽造防止オプション

AntiforgeryOptionsStartup.ConfigureServices でカスタマイズします。

services.AddAntiforgery(options => 
{
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});

次の表に示すように、CookieBuilder クラスのプロパティを使って偽造防止の Cookie プロパティを設定します。

オプション 説明
Cookie 偽造防止 cookie の作成に使用される設定を決定します。
FormFieldName ビューで偽造防止トークンをレンダリングするために偽造防止システムによって使用される非表示フォーム フィールドの名前。
HeaderName 偽造防止システムによって使用されるヘッダーの名前。 null の場合、システムではフォーム データのみが考慮されます。
SuppressXFrameOptionsHeader X-Frame-Options ヘッダーの生成を抑制するかどうかを指定します。 既定では、ヘッダーは値 "SAMEORIGIN" を使用して生成されます。 既定値は false です。

詳細については、「CookieAuthenticationOptions」を参照してください。

IAntiforgery を使用して偽造防止機能を構成する

IAntiforgery では、偽造防止機能を構成するための API が提供されます。 IAntiforgery は、Startup クラスの Configure メソッドで要求できます。

次の例では

  • アプリのホーム ページからミドルウェアを使って偽造防止トークンを生成し、cookie として応答で送信します。
  • この要求トークンは、AngularJS のセクションで説明した既定の Angular 名前付け規則を使い、JavaScript で読み取り可能な cookie として送信されます。
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
    app.Use(next => context =>
    {
        string path = context.Request.Path.Value;

        if (string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
        {
            var tokens = antiforgery.GetAndStoreTokens(context);
            context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, 
                new CookieOptions() { HttpOnly = false });
        }

        return next(context);
    });
}

偽造防止の検証を要求する

ValidateAntiForgeryToken は、個々のアクションやコントローラー、またはグローバルに適用できるアクション フィルターです。 このフィルターが適用されているアクションに対して行われた要求は、要求に有効な偽造防止トークンが含まれていない限り、ブロックされます。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel account)
{
    ManageMessageId? message = ManageMessageId.Error;
    var user = await GetCurrentUserAsync();

    if (user != null)
    {
        var result = 
            await _userManager.RemoveLoginAsync(
                user, account.LoginProvider, account.ProviderKey);

        if (result.Succeeded)
        {
            await _signInManager.SignInAsync(user, isPersistent: false);
            message = ManageMessageId.RemoveLoginSuccess;
        }
    }

    return RedirectToAction(nameof(ManageLogins), new { Message = message });
}

ValidateAntiForgeryToken 属性には、HTTP GET 要求を含む、マークするアクション メソッドへの要求に対するトークンが必要です。 ValidateAntiForgeryToken 属性がアプリのコントローラー全体に適用されている場合は、IgnoreAntiforgeryToken 属性でオーバーライドできます。

Note

ASP.NET Core では、GET 要求への自動的な偽造防止トークンの追加はサポートされていません。

安全でない HTTP メソッドについてのみ偽造防止トークンを自動的に検証する

ASP.NET Core アプリでは、安全な HTTP メソッド (GET、HEAD、OPTIONS、TRACE) には偽造防止トークンは生成されません。 ValidateAntiForgeryToken 属性を広範に適用してから IgnoreAntiforgeryToken 属性でオーバーライドするのではなく、AutoValidateAntiforgeryToken 属性を使用できます。 この属性は、ValidateAntiForgeryToken 属性と同じように動作しますが、次の HTTP メソッドを使用して行われた要求についてはトークンを要求しない点が異なります。

  • GET
  • HEAD
  • OPTIONS
  • TRACE

非 API のシナリオでは、AutoValidateAntiforgeryToken を広範に使うことをお勧めします。 この属性により、POST アクションが既定で保護されるようになります。 代わりの方法は、ValidateAntiForgeryToken が個々のアクション メソッドに適用されない限り、既定で偽造防止トークンを無視することです。 このシナリオでは、POST アクション メソッドが誤って保護されないままになり、アプリが CSRF 攻撃に対して脆弱になる可能性が高くなります。 すべての POST で、偽造防止トークンを送信する必要があります。

API には、トークンの cookie 以外の部分を送信するための自動メカニズムはありません。 実装は、クライアント コードの実装によって異なる場合があります。 いくつかの例を次に示します。

クラス レベルの例:

[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{

グローバルな例:

services.AddControllersWithViews(options =>
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

グローバルまたはコントローラーの偽造防止属性をオーバーライドする

IgnoreAntiforgeryToken フィルターは、特定のアクション (またはコントローラー) に対して偽造防止トークンを不要にするために使用されます。 このフィルターを適用すると、より高いレベル (グローバルまたはコントローラー上) で指定されている ValidateAntiForgeryToken および AutoValidateAntiforgeryToken フィルターがオーバーライドされます。

[Authorize]
[AutoValidateAntiforgeryToken]
public class ManageController : Controller
{
    [HttpPost]
    [IgnoreAntiforgeryToken]
    public async Task<IActionResult> DoSomethingSafe(SomeViewModel model)
    {
        // no antiforgery token required
    }
}

認証後にトークンを更新する

ユーザーが認証された後、ユーザーをビューまたは Razor Pages ページにリダイレクトすることによって、トークンを更新する必要があります。

JavaScript、AJAX、SPA

従来の HTML ベースのアプリでは、偽造防止トークンは非表示のフォーム フィールドを使用してサーバーに渡されます。 最新の JavaScript ベースのアプリと SPA では、多くの要求がプログラムによって行われます。 これらの AJAX 要求では、他の手法 (要求ヘッダーや cookie など) を使ってトークンを送信できます。

cookie が、認証トークンの格納と、サーバーでの API 要求の認証に使用されている場合、CSRF が問題になる可能性があります。 ローカル ストレージがトークンの格納に使われている場合は、ローカル ストレージの値はすべての要求でサーバーに自動的に送信されないため、CSRF の脆弱性が軽減される可能性があります。 ローカル ストレージを使用してクライアントに偽造防止トークンを格納し、要求ヘッダーとしてトークンを送信することをお勧めします。

JavaScript

JavaScript とビューを使うと、ビュー内からサービスを使ってトークンを作成できます。 ビューに IAntiforgery サービスを挿入し、GetAndStoreTokens を呼び出します。

@{
    ViewData["Title"] = "AJAX Demo";
}
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
@functions{
    public string GetAntiXsrfRequestToken()
    {
        return Xsrf.GetAndStoreTokens(Context).RequestToken;
    }
}

<input type="hidden" id="RequestVerificationToken" 
       name="RequestVerificationToken" value="@GetAntiXsrfRequestToken()">

<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<div class="row">
    <p><input type="button" id="antiforgery" value="Antiforgery"></p>
    <script>
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function() {
            if (xhttp.readyState == XMLHttpRequest.DONE) {
                if (xhttp.status == 200) {
                    alert(xhttp.responseText);
                } else {
                    alert('There was an error processing the AJAX request.');
                }
            }
        };

        document.addEventListener('DOMContentLoaded', function() {
            document.getElementById("antiforgery").onclick = function () {
                xhttp.open('POST', '@Url.Action("Antiforgery", "Home")', true);
                xhttp.setRequestHeader("RequestVerificationToken", 
                    document.getElementById('RequestVerificationToken').value);
                xhttp.send();
            }
        });
    </script>
</div>

この方法を使用すると、サーバーからの cookie の設定やクライアントからの読み取りを、直接処理する必要がなくなります。

前の例では、JavaScript を使用して、AJAX POST ヘッダーの非表示フィールドの値を読み取っています。

また、JavaScript で cookie のトークンにアクセスし、cookie の内容を使用して、トークンの値でヘッダーを作成することもできます。

context.Response.Cookies.Append("CSRF-TOKEN", tokens.RequestToken, 
    new Microsoft.AspNetCore.Http.CookieOptions { HttpOnly = false });

X-CSRF-TOKEN という名前のヘッダーでトークンを送信することがスクリプトで要求されている場合は、X-CSRF-TOKEN ヘッダーを検索するように偽造防止サービスを構成します。

services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");

次の例では、JavaScript を使用して、適切なヘッダーで AJAX 要求を作成しています。

function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) === ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) === 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

var csrfToken = getCookie("CSRF-TOKEN");

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
    if (xhttp.readyState === XMLHttpRequest.DONE) {
        if (xhttp.status === 204) {
            alert('Todo item is created successfully.');
        } else {
            alert('There was an error processing the AJAX request.');
        }
    }
};
xhttp.open('POST', '/api/items', true);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.setRequestHeader("X-CSRF-TOKEN", csrfToken);
xhttp.send(JSON.stringify({ "name": "Learn C#" }));

AngularJS

AngularJS では、CSRF に対処するために規則が使用されます。 サーバーが XSRF-TOKEN という名前で cookie を送信した場合、AngularJS$http サービスにより、サーバーに要求が送信されるときに、ヘッダーに cookie の値が追加されます。 このプロセスは自動です。 クライアントは、ヘッダーを明示的に設定する必要はありません。 ヘッダーの名前は X-XSRF-TOKEN です。 サーバーでこのヘッダーを検出し、その内容を検証する必要があります。

アプリケーションの起動時に ASP.NET Core API でこの規則を処理するには:

  • XSRF-TOKEN という名前の cookie でトークンを提供するようにアプリを構成します。
  • X-XSRF-TOKEN というヘッダーを探すように偽造防止サービスを構成します。これは、XSRF トークンを送信するための Angular の既定のヘッダー名です。
public void Configure(IApplicationBuilder app, IAntiforgery antiforgery)
{
    app.Use(next => context =>
    {
        string path = context.Request.Path.Value;

        if (
            string.Equals(path, "/", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(path, "/index.html", StringComparison.OrdinalIgnoreCase))
        {
            var tokens = antiforgery.GetAndStoreTokens(context);
            context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, 
                new CookieOptions() { HttpOnly = false });
        }

        return next(context);
    });
}

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

注意

要求ヘッダーとフォーム ペイロードの両方で偽造防止トークンが提供される場合、ヘッダー内のトークンのみが検証されます。

Windows 認証と偽造防止 cookie

Windows 認証を使用するときは、cookie の場合と同じ方法で、アプリケーション エンドポイントを CSRF 攻撃から保護する必要があります。 ブラウザーによって認証コンテキストがサーバーに暗黙的に送信されるため、エンドポイントを CSRF 攻撃から保護する必要があります。

偽造防止を強化する

IAntiforgeryAdditionalDataProvider 型を使用すると、開発者は、トークンごとに追加データをラウンドトリップすることで、CSRF 対策システムの動作を強化できます。 フィールド トークンを生成するたびに GetAdditionalData メソッドを呼び出し、戻り値を生成されるトークンに埋め込みます。 実装者は、タイムスタンプ、nonce、または他の値を返し、トークンの検証時に ValidateAdditionalData を呼び出してこのデータを検証できます。 クライアントのユーザー名は生成されたトークンに既に埋め込まれているので、この情報を含める必要はありません。 トークンに補足データが含まれていても、IAntiForgeryAdditionalDataProvider が構成されていない場合は、補足データは検証されません。

その他の技術情報