在 ASP.NET Core 中预防跨网站请求伪造 (XSRF/CSRF) 攻击

作者:Fiyaz HasanRick Anderson

跨网站请求伪造是一种针对 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 将发布到易受攻击的网站,而不是恶意网站。 这是 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 请求的状态。

CSRF 攻击可能会针对使用 Cookie 进行身份验证的 Web 应用,原因如下:

  • 浏览器存储 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 漏洞。 当令牌存储在 cookie 中时,CSRF 是一个令人担忧的问题。 有关详细信息,请参阅 GitHub 问题 SPA 代码示例添加两个 Cookie

多个应用托管在一个域中

共享托管环境容易受到会话劫持、登录 CSRF 和其他攻击。

尽管 example1.contoso.netexample2.contoso.net 是不同的主机,但 *.contoso.net 域下的主机之间存在隐式信任关系。 这种隐式信任关系允许可能不受信任的主机影响彼此的 Cookie(管理 AJAX 请求的同源策略不一定适用于 HTTP )。

利用同一域上托管的应用之间受信任的 Cookie 的攻击可以通过不共享域来防止。 当每个应用托管在自己的域中时,就没有隐式 cookie 信任关系可以利用。

ASP.NET Core 中的防伪造

警告

ASP.NET Core 使用 ASP.NET Core 数据保护实现防伪造。 必须将数据保护堆栈配置为在服务器场中工作。 有关详细信息,请参阅配置数据保护

Program.cs 中调用以下 API 之一时,防伪造中间件将添加到依赖关系注入容器:

有关详细信息,请参阅用最小 API 实现防伪

FormTagHelper 将防伪造令牌注入 HTML 窗体元素。 Razor 文件的以下标记将自动生成防伪造令牌:

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

同样,如果表单的方法不是 GET,则 IHtmlHelper.BeginForm 默认生成防伪造令牌。

<form> 标记包含 method="post" 属性且满足以下任一条件时,将针对 HTML 表单元素自动生成防伪造令牌:

  • 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
    

注意

Razor Pages 会自动防止 XSRF/CSRF。 有关详细信息,请参阅 XSRF/CSRF 和 Razor Pages

防范 CSRF 攻击的最常见方法是使用同步器同步器令牌模式 (STP)。 当用户请求包含表单数据的页面时,使用 STP:

  1. 服务器向客户端发送与当前用户 identity 相关联的令牌。
  2. 客户端将该令牌发送回服务器进行验证。
  3. 如果服务器收到的令牌与经过身份验证的用户 identity 不匹配,则请求会被拒绝。

令牌是唯一且不可预测的。 令牌还可用于确保对一系列请求进行正确的排序(例如,确保请求顺序为:第 1 页 > 第 2 页 > 第 3 页)。 ASP.NET Core MVC 和 Razor Pages 模板中的所有表单都会生成防伪造令牌。 下面这对视图示例生成防伪造令牌:

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

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

将防伪造令牌显式添加到 <form> 元素,而无需结合使用标记帮助程序与 HTML 帮助程序 @Html.AntiForgeryToken

<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 包含三个用于处理防伪造令牌的筛选器

使用 AddControllers 进行防伪造

调用AddControllers不会启用防伪造令牌。 AddControllersWithViews 必须调用才具有内置的防伪造令牌支持。

多个浏览器选项卡和同步器令牌模式

使用同步器令牌模式时,只有最近加载的页面才包含有效的防伪造令牌。 使用多个选项卡可能会有问题。 例如,如果用户打开多个选项卡:

  • 只有最近加载的选项卡包含有效的防伪造令牌。
  • 从以前加载的选项卡发出的请求失败并出现错误: Antiforgery token validation failed. The antiforgery cookie token and request token do not match

如果这会带来问题,请考虑替代 CSRF 保护模式。

使用 AntiforgeryOptions 配置防伪造

Program.cs 中自定义 AntiforgeryOptions

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 可以使用 WebApplication.ServicesProgram.cs 中请求。 以下示例使用应用 home 页中的中间件生成防伪造令牌,并将其作为 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 防护,该防护将默认读取名为 XSRF-TOKEN 的 cookie。

需要防伪造验证

ValidateAntiForgeryToken 操作筛选器可应用于单个操作、控制器或全局操作。 除非请求包含有效的防伪造令牌,否则对已应用此筛选器的操作的请求将被阻止:

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

    return RedirectToAction();
}

ValidateAntiForgeryToken 属性需要令牌才能请求它标记的操作方法,包括 HTTP GET 请求。 如果 ValidateAntiForgeryToken 属性应用于应用的控制器,则可使用 IgnoreAntiforgeryToken 属性替代该属性。

仅自动验证不安全 HTTP 方法的防伪造令牌

可以使用 AutoValidateAntiforgeryToken 属性,而不是广泛应用 ValidateAntiForgeryToken 属性,然后使用 IgnoreAntiforgeryToken 属性将其替代。 此属性与 ValidateAntiForgeryToken 属性的工作原理相同,只不过它不需要令牌即可使用以下 HTTP 方法发出请求:

  • GET
  • HEAD
  • OPTIONS
  • TRACE

建议将 AutoValidateAntiforgeryToken 广泛用于非 API 方案。 此属性可确保 POST 操作在默认情况下受到保护。 替代方法是默认忽略防伪造令牌,除非 ValidateAntiForgeryToken 应用于单个操作方法。 在这种情况下,更有可能错误地使 POST 操作方法不受保护,从而使应用容易受到 CSRF 攻击。 所有 POST 都应发送防伪造令牌。

API 没有自动机制来发送令牌的非 cookie 部分。 该实现可能取决于客户端代码实现。 下面是一些示例:

类级别示例:

[AutoValidateAntiforgeryToken]
public class HomeController : Controller

全局示例:

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

替代全局或控制器防伪造属性

如果使用 IgnoreAntiforgeryToken 筛选器,给定操作(或控制器)无需防伪造令牌。 应用后,此筛选器将替代更高级别(全局或控制器上)指定的 ValidateAntiForgeryTokenAutoValidateAntiforgeryToken 筛选器。

[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,也无需从客户端读取 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}`
}

注意

当在请求标头和表单有效负载中同时提供防伪造令牌时,只会验证标头中的令牌。

使用最少 API 进行防伪造

调用 AddAntiforgeryUseAntiforgery(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

请考虑下面的示例:

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 需要有效的防伪令牌。
  • /todo2 需要有效的防伪令牌,因为会调用 DisableAntiforgery
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));

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

发布到:

  • / 终结点生成的表单中的 /todo 成功,因为防伪令牌有效。
  • /SkipToken 生成的表单中的 /todo 失败,因为未包含防伪。
  • /DisableAntiforgery 终结点生成的表单中的 /todo2 成功,因为不需要防伪。
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 浏览器会随着对网站的每个请求自动发送某些类型的身份验证令牌。 这种形式的攻击也称为一键式攻击或会话控制,因为该攻击利用了用户以前经过身份验证的会话

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 将发布到易受攻击的网站,而不是恶意网站。 这是 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 请求的状态。

CSRF 攻击可能会针对使用 Cookie 进行身份验证的 Web 应用,原因如下:

  • 浏览器存储 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 漏洞。 当令牌存储在 cookie 中时,CSRF 是一个令人担忧的问题。 有关详细信息,请参阅 GitHub 问题 SPA 代码示例添加两个 Cookie

多个应用托管在一个域中

共享托管环境容易受到会话劫持、登录 CSRF 和其他攻击。

尽管 example1.contoso.netexample2.contoso.net 是不同的主机,但 *.contoso.net 域下的主机之间存在隐式信任关系。 这种隐式信任关系允许可能不受信任的主机影响彼此的 Cookie(管理 AJAX 请求的同源策略不一定适用于 HTTP )。

利用同一域上托管的应用之间受信任的 Cookie 的攻击可以通过不共享域来防止。 当每个应用托管在自己的域中时,就没有隐式 cookie 信任关系可以利用。

ASP.NET Core 中的防伪造

警告

ASP.NET Core 使用 ASP.NET Core 数据保护实现防伪造。 必须将数据保护堆栈配置为在服务器场中工作。 有关详细信息,请参阅配置数据保护

Program.cs 中调用以下 API 之一时,防伪造中间件将添加到依赖关系注入容器:

FormTagHelper 将防伪造令牌注入 HTML 窗体元素。 Razor 文件的以下标记将自动生成防伪造令牌:

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

同样,如果表单的方法不是 GET,则 IHtmlHelper.BeginForm 默认生成防伪造令牌。

<form> 标记包含 method="post" 属性且满足以下任一条件时,将针对 HTML 表单元素自动生成防伪造令牌:

  • 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
    

注意

Razor Pages 会自动防止 XSRF/CSRF。 有关详细信息,请参阅 XSRF/CSRF 和 Razor Pages

防御 CSRF 攻击的最常见方法是使用同步器令牌模式 (STP)。 当用户请求包含表单数据的页面时,使用 STP:

  1. 服务器向客户端发送与当前用户 identity 相关联的令牌。
  2. 客户端将该令牌发送回服务器进行验证。
  3. 如果服务器收到的令牌与经过身份验证的用户 identity 不匹配,则请求会被拒绝。

令牌是唯一且不可预测的。 令牌还可用于确保对一系列请求进行正确的排序(例如,确保请求顺序为:第 1 页 > 第 2 页 > 第 3 页)。 ASP.NET Core MVC 和 Razor Pages 模板中的所有表单都会生成防伪造令牌。 下面这对视图示例生成防伪造令牌:

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

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

将防伪造令牌显式添加到 <form> 元素,而无需结合使用标记帮助程序与 HTML 帮助程序 @Html.AntiForgeryToken

<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 包含三个用于处理防伪造令牌的筛选器

使用 AddControllers 进行防伪造

调用AddControllers不会启用防伪造令牌。 AddControllersWithViews 必须调用才具有内置的防伪造令牌支持。

多个浏览器选项卡和同步器令牌模式

使用同步器令牌模式时,只有最近加载的页面才包含有效的防伪造令牌。 使用多个选项卡可能会有问题。 例如,如果用户打开多个选项卡:

  • 只有最近加载的选项卡包含有效的防伪造令牌。
  • 从以前加载的选项卡发出的请求失败并出现错误: Antiforgery token validation failed. The antiforgery cookie token and request token do not match

如果这会带来问题,请考虑替代 CSRF 保护模式。

使用 AntiforgeryOptions 配置防伪造

Program.cs 中自定义 AntiforgeryOptions

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 可以使用 WebApplication.ServicesProgram.cs 中请求。 以下示例使用应用 home 页中的中间件生成防伪造令牌,并将其作为 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 防护,该防护将默认读取名为 XSRF-TOKEN 的 cookie。

需要防伪造验证

ValidateAntiForgeryToken 操作筛选器可应用于单个操作、控制器或全局操作。 除非请求包含有效的防伪造令牌,否则对已应用此筛选器的操作的请求将被阻止:

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

    return RedirectToAction();
}

ValidateAntiForgeryToken 属性需要令牌才能请求它标记的操作方法,包括 HTTP GET 请求。 如果 ValidateAntiForgeryToken 属性应用于应用的控制器,则可使用 IgnoreAntiforgeryToken 属性替代该属性。

仅自动验证不安全 HTTP 方法的防伪造令牌

可以使用 AutoValidateAntiforgeryToken 属性,而不是广泛应用 ValidateAntiForgeryToken 属性,然后使用 IgnoreAntiforgeryToken 属性将其替代。 此属性与 ValidateAntiForgeryToken 属性的工作原理相同,只不过它不需要令牌即可使用以下 HTTP 方法发出请求:

  • GET
  • HEAD
  • OPTIONS
  • TRACE

建议将 AutoValidateAntiforgeryToken 广泛用于非 API 方案。 此属性可确保 POST 操作在默认情况下受到保护。 替代方法是默认忽略防伪造令牌,除非 ValidateAntiForgeryToken 应用于单个操作方法。 在这种情况下,更有可能错误地使 POST 操作方法不受保护,从而使应用容易受到 CSRF 攻击。 所有 POST 都应发送防伪造令牌。

API 没有自动机制来发送令牌的非 cookie 部分。 该实现可能取决于客户端代码实现。 下面是一些示例:

类级别示例:

[AutoValidateAntiforgeryToken]
public class HomeController : Controller

全局示例:

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

替代全局或控制器防伪造属性

如果使用 IgnoreAntiforgeryToken 筛选器,给定操作(或控制器)无需防伪造令牌。 应用后,此筛选器将替代更高级别(全局或控制器上)指定的 ValidateAntiForgeryTokenAutoValidateAntiforgeryToken 筛选器。

[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,也无需从客户端读取 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}`
}

注意

当在请求标头和表单有效负载中同时提供防伪造令牌时,只会验证标头中的令牌。

使用最少 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 浏览器会随着对网站的每个请求自动发送某些类型的身份验证令牌。 这种形式的攻击也称为一键式攻击或会话控制,因为该攻击利用了用户以前经过身份验证的会话

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 将发布到易受攻击的网站,而不是恶意网站。 这是 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 请求的状态。

CSRF 攻击可能会针对使用 Cookie 进行身份验证的 Web 应用,原因如下:

  • 浏览器存储 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 漏洞。 当令牌存储在 cookie 中时,CSRF 是一个令人担忧的问题。 有关详细信息,请参阅 GitHub 问题 SPA 代码示例添加两个 Cookie

多个应用托管在一个域中

共享托管环境容易受到会话劫持、登录 CSRF 和其他攻击。

尽管 example1.contoso.netexample2.contoso.net 是不同的主机,但 *.contoso.net 域下的主机之间存在隐式信任关系。 这种隐式信任关系允许可能不受信任的主机影响彼此的 Cookie(管理 AJAX 请求的同源策略不一定适用于 HTTP )。

利用同一域上托管的应用之间受信任的 Cookie 的攻击可以通过不共享域来防止。 当每个应用托管在自己的域中时,就没有隐式 cookie 信任关系可以利用。

ASP.NET Core 中的防伪造

警告

ASP.NET Core 使用 ASP.NET Core 数据保护实现防伪造。 必须将数据保护堆栈配置为在服务器场中工作。 有关详细信息,请参阅配置数据保护

Program.cs 中调用以下 API 之一时,防伪造中间件将添加到依赖关系注入容器:

FormTagHelper 将防伪造令牌注入 HTML 窗体元素。 Razor 文件的以下标记将自动生成防伪造令牌:

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

同样,如果表单的方法不是 GET,则 IHtmlHelper.BeginForm 默认生成防伪造令牌。

<form> 标记包含 method="post" 属性且满足以下任一条件时,将针对 HTML 表单元素自动生成防伪造令牌:

  • 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
    

注意

Razor Pages 会自动防止 XSRF/CSRF。 有关详细信息,请参阅 XSRF/CSRF 和 Razor Pages

防御 CSRF 攻击的最常见方法是使用同步器令牌模式 (STP)。 当用户请求包含表单数据的页面时,使用 STP:

  1. 服务器向客户端发送与当前用户 identity 相关联的令牌。
  2. 客户端将该令牌发送回服务器进行验证。
  3. 如果服务器收到的令牌与经过身份验证的用户 identity 不匹配,则请求会被拒绝。

令牌是唯一且不可预测的。 令牌还可用于确保对一系列请求进行正确的排序(例如,确保请求顺序为:第 1 页 > 第 2 页 > 第 3 页)。 ASP.NET Core MVC 和 Razor Pages 模板中的所有表单都会生成防伪造令牌。 下面这对视图示例生成防伪造令牌:

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

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

将防伪造令牌显式添加到 <form> 元素,而无需结合使用标记帮助程序与 HTML 帮助程序 @Html.AntiForgeryToken

<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 包含三个用于处理防伪造令牌的筛选器

使用 AddControllers 进行防伪造

调用AddControllers不会启用防伪造令牌。 AddControllersWithViews 必须调用才具有内置的防伪造令牌支持。

多个浏览器选项卡和同步器令牌模式

使用同步器令牌模式时,只有最近加载的页面才包含有效的防伪造令牌。 使用多个选项卡可能会有问题。 例如,如果用户打开多个选项卡:

  • 只有最近加载的选项卡包含有效的防伪造令牌。
  • 从以前加载的选项卡发出的请求失败并出现错误: Antiforgery token validation failed. The antiforgery cookie token and request token do not match

如果这会带来问题,请考虑替代 CSRF 保护模式。

使用 AntiforgeryOptions 配置防伪造

Program.cs 中自定义 AntiforgeryOptions

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 可以使用 WebApplication.ServicesProgram.cs 中请求。 以下示例使用应用 home 页中的中间件生成防伪造令牌,并将其作为 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 防护,该防护将默认读取名为 XSRF-TOKEN 的 cookie。

需要防伪造验证

ValidateAntiForgeryToken 操作筛选器可应用于单个操作、控制器或全局操作。 除非请求包含有效的防伪造令牌,否则对已应用此筛选器的操作的请求将被阻止:

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

    return RedirectToAction();
}

ValidateAntiForgeryToken 属性需要令牌才能请求它标记的操作方法,包括 HTTP GET 请求。 如果 ValidateAntiForgeryToken 属性应用于应用的控制器,则可使用 IgnoreAntiforgeryToken 属性替代该属性。

仅自动验证不安全 HTTP 方法的防伪造令牌

可以使用 AutoValidateAntiforgeryToken 属性,而不是广泛应用 ValidateAntiForgeryToken 属性,然后使用 IgnoreAntiforgeryToken 属性将其替代。 此属性与 ValidateAntiForgeryToken 属性的工作原理相同,只不过它不需要令牌即可使用以下 HTTP 方法发出请求:

  • GET
  • HEAD
  • OPTIONS
  • TRACE

建议将 AutoValidateAntiforgeryToken 广泛用于非 API 方案。 此属性可确保 POST 操作在默认情况下受到保护。 替代方法是默认忽略防伪造令牌,除非 ValidateAntiForgeryToken 应用于单个操作方法。 在这种情况下,更有可能错误地使 POST 操作方法不受保护,从而使应用容易受到 CSRF 攻击。 所有 POST 都应发送防伪造令牌。

API 没有自动机制来发送令牌的非 cookie 部分。 该实现可能取决于客户端代码实现。 下面是一些示例:

类级别示例:

[AutoValidateAntiforgeryToken]
public class HomeController : Controller

全局示例:

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

替代全局或控制器防伪造属性

如果使用 IgnoreAntiforgeryToken 筛选器,给定操作(或控制器)无需防伪造令牌。 应用后,此筛选器将替代更高级别(全局或控制器上)指定的 ValidateAntiForgeryTokenAutoValidateAntiforgeryToken 筛选器。

[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,也无需从客户端读取 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}`
}

Windows 身份验证和防伪造 Cookie

使用 Windows 身份验证时,必须像保护 Cookie 一样保护应用程序终结点免受 CSRF 攻击。 浏览器将身份验证上下文隐式发送到服务器,因此需要保护终结点免受 CSRF 攻击。

扩展防伪造

IAntiforgeryAdditionalDataProvider 类型允许开发人员通过往返传递每个令牌中的附加数据来扩展反 CSRF 系统的行为。 每次生成域令牌时,将调用 GetAdditionalData 方法,并且返回值嵌入到生成的令牌中。 实施者可以返回时间戳、nonce 或任何其他值,然后在验证令牌时调用 ValidateAdditionalData 来验证此数据。 客户端的用户名已嵌入到生成的令牌中,因此无需包含此信息。 如果令牌包含补充数据,但没有配置 IAntiForgeryAdditionalDataProvider,则不验证补充数据。

其他资源

跨网站请求伪造(也称为 XSRF 或 CSRF)是一种针对 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 将发布到易受攻击的网站,而不是恶意网站。 这是 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 请求的状态。

CSRF 攻击可能会针对使用 Cookie 进行身份验证的 Web 应用,原因如下:

  • 浏览器存储 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 漏洞。 当令牌存储在 cookie 中时,CSRF 是一个令人担忧的问题。 有关详细信息,请参阅 GitHub 问题 SPA 代码示例添加两个 Cookie

多个应用托管在一个域中

共享托管环境容易受到会话劫持、登录 CSRF 和其他攻击。

尽管 example1.contoso.netexample2.contoso.net 是不同的主机,但 *.contoso.net 域下的主机之间存在隐式信任关系。 这种隐式信任关系允许可能不受信任的主机影响彼此的 Cookie(管理 AJAX 请求的同源策略不一定适用于 HTTP )。

利用同一域上托管的应用之间受信任的 Cookie 的攻击可以通过不共享域来防止。 当每个应用托管在自己的域中时,就没有隐式 cookie 信任关系可以利用。

ASP.NET Core 防伪造配置

警告

ASP.NET Core 使用 ASP.NET Core 数据保护实现防伪造。 必须将数据保护堆栈配置为在服务器场中工作。 有关详细信息,请参阅配置数据保护

Startup.ConfigureServices 中调用以下 API 之一时,防伪造中间件将添加到依赖关系注入容器:

在 ASP.NET Core 2.0 或更高版本中,FormTagHelper 将防伪造令牌注入 HTML 表单元素。 Razor 文件的以下标记将自动生成防伪造令牌:

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

同样,如果表单的方法不是 GET,则 IHtmlHelper.BeginForm 默认生成防伪造令牌。

<form> 标记包含 method="post" 属性且满足以下任一条件时,将针对 HTML 表单元素自动生成防伪造令牌:

  • 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
    

注意

Razor Pages 会自动防止 XSRF/CSRF。 有关详细信息,请参阅 XSRF/CSRF 和 Razor Pages

防御 CSRF 攻击的最常见方法是使用同步器令牌模式 (STP)。 当用户请求包含表单数据的页面时,使用 STP:

  1. 服务器向客户端发送与当前用户 identity 相关联的令牌。
  2. 客户端将该令牌发送回服务器进行验证。
  3. 如果服务器收到的令牌与经过身份验证的用户 identity 不匹配,则请求会被拒绝。

令牌是唯一且不可预测的。 令牌还可用于确保对一系列请求进行正确的排序(例如,确保请求顺序为:第 1 页 > 第 2 页 > 第 3 页)。 ASP.NET Core MVC 和 Razor Pages 模板中的所有表单都会生成防伪造令牌。 下面这对视图示例生成防伪造令牌:

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

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

将防伪造令牌显式添加到 <form> 元素,而无需结合使用标记帮助程序与 HTML 帮助程序 @Html.AntiForgeryToken

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

在上述每个示例中,ASP.NET Core 添加类似于以下示例的隐藏表单域:

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

ASP.NET Core 包含三个用于处理防伪造令牌的筛选器

防伪造选项

Startup.ConfigureServices 中自定义 AntiforgeryOptions

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。 可以在 Startup 类的 Configure 方法中请求 IAntiforgery

在下面的示例中:

  • 应用 home 页中的中间件用于生成防伪造令牌,并将其作为 cookie 在响应中发送。
  • 请求令牌以 JavaScript 可读 cookie 发送,并采用AngularJS 部分所述的默认 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);
    });
}

需要防伪造验证

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 属性替代该属性。

注意

ASP.NET Core 不支持自动将防伪造令牌添加到 GET 请求。

仅自动验证不安全 HTTP 方法的防伪造令牌

ASP.NET Core 应用不会为安全的 HTTP 方法(GET、HEAD、OPTIONS 和 TRACE)生成防伪造令牌。 可以使用 AutoValidateAntiforgeryToken 属性,而不是广泛应用 ValidateAntiForgeryToken 属性,然后使用 IgnoreAntiforgeryToken 属性将其替代。 此属性与 ValidateAntiForgeryToken 属性的工作原理相同,只不过它不需要令牌即可使用以下 HTTP 方法发出请求:

  • GET
  • HEAD
  • OPTIONS
  • TRACE

建议将 AutoValidateAntiforgeryToken 广泛用于非 API 方案。 此属性可确保 POST 操作在默认情况下受到保护。 替代方法是默认忽略防伪造令牌,除非 ValidateAntiForgeryToken 应用于单个操作方法。 在这种情况下,更有可能错误地使 POST 操作方法不受保护,从而使应用容易受到 CSRF 攻击。 所有 POST 都应发送防伪造令牌。

API 没有自动机制来发送令牌的非 cookie 部分。 该实现可能取决于客户端代码实现。 下面是一些示例:

类级别示例:

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

全局示例:

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

替代全局或控制器防伪造属性

如果使用 IgnoreAntiforgeryToken 筛选器,给定操作(或控制器)无需防伪造令牌。 应用后,此筛选器将替代更高级别(全局或控制器上)指定的 ValidateAntiForgeryTokenAutoValidateAntiforgeryToken 筛选器。

[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,也无需从客户端读取 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 的标头,该标头是 Angular 用于发送 XSRF 令牌的默认标头名称。
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,则不验证补充数据。

其他资源