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

作者:Fiyaz HasanRick AndersonSteve Smith

跨网站请求伪造(也称为 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个 s

多个应用托管在一个域中

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

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

利用同一域上托管的应用之间受信任的 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 页面 会自动受到 XSRF/CSRF 的保护。 有关详细信息,请参阅 XSRF/CSRF 和 Razor Pages

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

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

令牌是唯一且不可预测的。 令牌还可用于确保对一系列请求进行正确的排序(例如,确保请求顺序为:第 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 进行反forgery

调用AddControllers不会启用反forgery 令牌。 AddControllersWithViews 必须调用以具有内置的反forgery 令牌支持。

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

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

  • 只有最近加载的选项卡包含有效的反forgery 令牌。
  • 以前加载的选项卡发出的请求失败,并出现错误: 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;
});

使用类的属性设置反forgery Cookie 属性 CookieBuilder ,如下表所示。

选项 说明
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 防护,该防护将默认读取名为 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 的反forgery

Minimal APIs不支持使用包含的筛选器 (ValidateAntiForgeryTokenAutoValidateAntiforgeryTokenIgnoreAntiforgeryTokenIAntiforgery) 提供了验证请求所需的 API。

以下示例创建一个筛选器,用于验证反forgery 令牌:

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 身份验证和反forgery cookies

使用 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个 s

多个应用托管在一个域中

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

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

利用同一域上托管的应用之间受信任的 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 页面 会自动受到 XSRF/CSRF 的保护。 有关详细信息,请参阅 XSRF/CSRF 和 Razor Pages

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

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

令牌是唯一且不可预测的。 令牌还可用于确保对一系列请求进行正确的排序(例如,确保请求顺序为:第 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 的 Antiforgery

调用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设置反forgery 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 防护,该防护将默认读取名为 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 身份验证和反forgery cookies

使用 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个 S

多个应用托管在一个域中

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

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

利用同一域上托管的应用之间受信任的 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 页面 会自动受到 XSRF/CSRF 的保护。 有关详细信息,请参阅 XSRF/CSRF 和 Razor Pages

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

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

令牌是唯一且不可预测的。 令牌还可用于确保对一系列请求进行正确的排序(例如,确保请求顺序为:第 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;
});

使用类的属性设置反forgery Cookie 属性 CookieBuilder ,如下表所示。

选项 说明
Cookie 确定用于创建防伪造 cookie 的设置。
FormFieldName 防伪造系统用于在视图中呈现防伪造令牌的隐藏表单域的名称。
HeaderName 防伪造系统使用的标头的名称。 如果为 null,则系统仅考虑表单数据。
SuppressXFrameOptionsHeader 指定是否禁止生成 X-Frame-Options 标头。 默认情况下,标头是使用值“SAMEORIGIN”生成的。 默认为 false

有关详细信息,请参阅 CookieAuthenticationOptions

使用 IAntiforgery 配置防伪造功能

IAntiforgery 提供用于配置防伪造功能的 API。 可以在 Startup 类的 Configure 方法中请求 IAntiforgery

如下示例中:

  • 应用的主页中的中间件用于生成防伪令牌,并将其作为响应发送。cookie
  • 请求令牌以 JavaScript 可cookie读的形式发送,其中包含Angular部分中介绍JS的默认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

JS Angular使用约定来处理 CSRF。 如果服务器发送名称为 acookie,则Angular$httpJS服务在向服务器发送请求时将该值添加到cookieXSRF-TOKEN标头。 此过程是自动的。 客户端不需要显式设置标头。 标头名称为 X-XSRF-TOKEN。 服务器应检测此标头并验证其内容。

要使 ASP.NET Core API 在应用程序启动时使用此约定:

  • 将应用配置为在名为 XSRF-TOKEN 的 cookie 中提供令牌。
  • 配置反forgery 服务以查找名为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 身份验证和反forgery cookies

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

扩展防伪造

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

其他资源