65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ASP.NET Core、JavaScript 和 Angular 防止 CSRF 攻击

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2021年3月21日

CPOL

11分钟阅读

viewsIcon

16267

如何在 ASP.NET Core、JavaScript 和 Angular 中防止跨站请求伪造攻击

目录

引言

跨站请求伪造(Cross-Site Request Forgery),也称为 CSRF(发音为 “See-Surf”)、XSRF、一键式攻击(One-Click Attack)和会话劫持(Session Riding),是一种攻击类型,攻击者强迫用户在已登录的应用程序中执行非自愿的操作。攻击者诱骗用户以他们的名义执行操作。这种攻击的影响取决于用户拥有的权限级别。例如,在一个易受攻击的银行网站上,攻击者可以从受害者的账户中转移一笔钱,或完全接管整个账户。

关于源代码

本文示例的源代码可在 GitHub 上通过以下链接获取

仓库中有五个项目

  • 攻击示例项目,AttackSample.AttackerAppAttackSample.VulnerableApp,展示了对一个易受攻击的应用发起的有效 CSRF 攻击。
  • 安全示例项目,SecureSample.SecureAppSecureSample.AttackerApp,展示了对一个受保护的应用发起的失败的 CSRF 攻击。
  • Angular 项目,SecureSample.AngularApp,这是一个独立的项目。

CSRF 示例

以银行网站为例,攻击者可能会诱骗用户加载某个网站或链接,其中包含一个脚本,该脚本向银行网站发出伪造的转账请求。如果用户当前已登录银行网站,且该网站易受此类攻击,那么攻击者可能会成功。

例如,如果银行网站允许以下请求进行转账

GET http://vulnerable-bank.com/transfer?amount=1000&to=12345

攻击者可以将自己的银行账号填入“to”字段,然后引导用户访问一个页面,该页面包含一个发出上述请求的脚本,或者诱导他/她点击某个特定链接,例如

<a href="http://vulnerable-bank.com/transfer?amount=1000&to=98765">Read More!</a>

或者将请求嵌入一个伪造的 0x0 图像中

<img src="http://vulnerable-bank.com/transfer?amount=1000&to=98765" width="0" height="0" />

攻击者可以将其代码包含在用户经常访问的网站中(例如,在一些下载论坛中放置一个链接),或者借助一些社会工程学手段(如通过电子邮件或聊天发送链接)来分发他的页面。

如果用户已经登录到银行账户,当他/她打开该页面或点击伪造链接时,浏览器会自动包含目标网站的 cookie 和其他数据,并执行伪造的请求,从而导致转账请求成功。

攻击剖析

总而言之,一次成功的 CSRF 攻击包括

  • 一个易受攻击的网站
  • 一个当前已登录该网站的用户
  • 浏览器可能在请求中包含的会话 cookie 和其他用户 cookie
  • 易于预测的请求参数
  • 用户访问一个有害页面或点击一个伪造链接,从而向易受攻击的网站执行一个伪造的请求

保护您的 Web 应用程序

要保护您的 Web 应用程序免受此类攻击,请始终

  • 使用不可预测的参数。让攻击者难以模拟或构造对您应用程序的请求。使用令牌是不可预测参数的一个例子,我们很快就会解释。
  • 在任何情况、任何步骤中都进行严格验证。

CSRF 攻击实战

在我们的代码示例中,我们有两个 AttackSample 项目,一个是易受攻击的应用程序,另一个是攻击者应用程序。在易受攻击的应用程序中,用户经过身份验证,并创建了一个 cookie 来保存用户数据

[HttpGet]
public bool IsAuthenticated()
{
  return Context.Request.Cookies["IsAuthenticated"] == "1";
}

[HttpPost]
public IActionResult Login()
{
  Context.Response.Cookies.Append("IsAuthenticated", "1");
  return RedirectToAction(nameof(Index), "Home");
}

[HttpPost]
public IActionResult Logout()
{
  Context.Response.Cookies.Delete("IsAuthenticated");
  return RedirectToAction(nameof(Index), "Home");
}

该应用程序维护一个 balance 对象,并允许对借记和贷记操作进行易于预测的请求

[HttpGet]
public int Balance()
{
  return CurrentBalance;
}

[HttpPost]
public int Debit(int amount)
{
  CurrentBalance -= amount;
  return Balance();
}

[HttpPost]
public int Credit(int amount)
{
  CurrentBalance += amount;
  return Balance();
}

另一方面,攻击者通过承诺用户点击链接即可获得礼物来诱骗用户执行伪造的请求

<form method="post" action="https://:62833/api/Debit">
  <input type="hidden" name="amount" value="50" />

  <button type="submit">Click here to win a free iPhone!</button>
</form>

用户被诱骗执行伪造请求,导致金额成功转移。

防伪令牌

保护您的网站免受这类攻击的关键,是使用不可预测的请求参数。其中一种不可预测的参数就是防伪令牌。

防伪令牌,也称为 CSRF 令牌,是由服务器端应用程序为客户端后续的 HTTP 请求生成的唯一的、秘密的、不可预测的参数。当该请求发出时,服务器会根据期望值验证此参数,如果令牌缺失或无效,则拒绝该请求。

所以,基本上,以下请求

GET http://bank.com/transfer?amount=1000&to=12345

将会扩展为带有第三个参数

GET http://bank.com/transfer?amount=1000&to=12345&token=32465468465468465165484654768732467655465

该令牌非常庞大且无法猜测。服务器仅为后续请求包含该令牌,并且每次提供页面/表单时都会生成一个新的令牌。

从技术上讲,防伪令牌并不是在查询字符串中发送的参数。实际上,它是一个表示为隐藏字段的 cookie,您在表单内生成它。当表单提交时,这个值会作为请求头的一部分随请求一起发送。服务器端代码将检查请求并验证从客户端发送的值。

ASP.NET Core 中的防伪

默认情况下,新的 ASP.NET Core Razor 引擎会为页面表单包含一个防伪令牌,您所需要做的就是添加相应的验证。尽管如此,接下来的几节将告诉您如何在您的应用中生成防伪令牌以及如何验证它们。

令牌生成:手动方式

生成和验证防伪令牌有两种方法,我们将从手动、不方便的方式开始。这可以通过使用 IAntiForgery 服务来完成。

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Csrf
@functions {
  public string GenerateCsrfToken()
  {
    return Csrf.GetAndStoreTokens(Context).RequestToken;
  }
}

<form method="post">
  <input type="hidden" id="RequestVerificationToken" 
   name="RequestVerificationToken" value="@GenerateCsrfToken()" />
</form>

GetAndStoreTokens 方法会生成一个请求令牌,并将其存储在响应 cookie 中。您可以通过 RequestToken 属性访问生成的令牌。

生成的隐藏字段将如下所示(为清晰起见,令牌已缩写)

<input type="hidden" id="RequestVerificationToken" 
name="RequestVerificationToken" value="CfDJ8El14QZHDe5Dtl0m3qOu6_PbEHcKAJ5ZjSRj6iF...">

另一种更清晰的手动生成 CSRF 令牌的方法是使用 MVC HTML 帮助器

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

您可以使用 Chrome 开发者工具检查生成的 cookie

令牌生成:自动方式

正如我们之前所说,新的 ASP.NET Core Razor 引擎总是会为您生成 CSRF 令牌,但是,您仍然可以控制令牌的生成过程。

您可以使用 AddAntiforgery 方法来自定义 Razor 页面的令牌生成过程,该方法可以在您的 Startup.ConfigureServices 方法中调用。

services.AddAntiforgery(options =>
{
  options.FormFieldName = "AntiForgeryFieldName";
  options.HeaderName = "AntiForgeryHeaderName";
  options.Cookie.Name = "AntiForgeryCookieName";
});

前面的代码将为令牌生成一个具有指定名称的隐藏字段,并且令牌将随请求以指定的请求头名称发送。

<input name="AntiForgeryFieldName" type="hidden" value="CfDJ8N1DZWaKEuhDio...">

默认情况下,ASP.NET Core 中生成的 cookie 名称是 “.AspNetCore.Antiforgery.<hash>”,字段名称是“__RequestVerificationToken”,请求头名称是“RequestVerificationToken”。

令牌验证

现在是下一步,令牌验证。让我们从常规、不方便的方式开始。在您的目标操作中,您可以使用以下代码进行令牌验证

private Microsoft.AspNetCore.Antiforgery.IAntiforgery Csrf { get; set; }

public ApiController(Microsoft.AspNetCore.Antiforgery.IAntiforgery csrf)
{
  this.Csrf = csrf;
}

private async Task<bool> ValidateAntiForgeryToken()
{
  try
  {
    await Csrf.ValidateRequestAsync(this.HttpContext);
    return true;
  }
  catch (Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException)
  {
    return false;
  }
}

[HttpPost]
public async Task<ActionResult<int>> Debit(int amount)
{
  if (false == await ValidateAntiForgeryToken())
    return BadRequest();

  // action logic here
}

在前面的代码中,我们首先定义了我们的 IAntiforgery 服务。该服务允许您使用 ValidateRequestAsync 方法验证给定的请求,当令牌无效时,该方法会抛出 AntiforgeryValidationException 异常。我们在 Debit 方法的一开始就调用了这个函数,并对无效令牌返回 400 Bad Request 响应。

另一种更清晰的验证 CSRF 令牌的方法是使用 ValidateAntiForgeryToken 特性

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult<int>> Debit(int amount)

以上代码将执行与手动使用 IAntiforgery 服务代码相同的功能。

现在,当我们运行我们的应用程序时,我们可以看到不同之处。该应用程序已受到保护,可抵御 CSRF 攻击,攻击者无法向您的应用发起请求。

值得一提的是,ValidateAntiForgeryToken 可以应用于控制器类,并将在所有端点上引发 CSRF 验证。我们也有一些替代 ValidateAntiForgeryToken 特性的方法

  • AutoValidateAntiForgeryToken 特性:将自动验证除 GETHEADOPTIONSTRACE 之外的所有 HTTP 方法的端点。
  • IgnoreAntiforgeryToken 特性:如果父类使用了 ValidateantiForgeryTokenAutoValidateAntiForgeryToken 进行修饰,则该特性将使某个方法免于验证。

JavaScript 中的防伪

让我们再看一个例子。假设您正在使用 JavaScript 访问您的 API,并且您使用以下代码来调用 credit 方法

function request(url) {
  let url = location.origin + "/api/credit?amount=10";

  var request = {};
  request.url = url;
  request.type = 'POST';
  request.success = function (balance) {
    $('#balance')[0].innerText = balance;
  };
  request.error = function (xhr) {
    alert(`${xhr.status} ${xhr.statusText}`);
  };

  $.ajax(request);
}

当防伪验证生效时,您会收到一个 400 bad request 错误,这是预料之中的,因为 ASP.NET Core 引擎找不到 CSRF 令牌头。

为了解决这个问题,我们必须手动将我们的 CSRF 令牌添加到请求头列表中。对我们的代码做一个小小的改动就能解决问题

function request(url) {
  let url = location.origin + "/api/debit?amount=10";

  var request = {};
  request.url = url;
  request.type = 'POST';
  request.headers = {
    'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val()
  };
  request.success = function (balance) {
    $('#balance')[0].innerText = balance;
  };
  request.error = function (xhr) {
    alert(`${xhr.status} ${xhr.statusText}`);
  };

  $.ajax(request);
}

如前所述,CSRF 令牌的默认请求头名称是“RequestVerificationToken”。如果您出于任何原因更改了默认的请求头名称

services.AddAntiforgery(options =>
{
  options.HeaderName = "AntiForgeryHeaderName";
});

那么您也必须更改 JavaScript 代码中的值

request.headers = {
  'AntiForgeryHeaderName': $('input[name="__RequestVerificationToken"]').val()
};

Angular 中的防伪

通常,当从 Angular 应用访问受 CSRF 保护的端点时,如果您没有指定 CSRF 请求头,您将收到 400 bad request 错误。

要处理这个问题,您必须了解以下几点

  • 只有当 CSRF 令牌作为 cookie 存储在 Angular 的专用名称 “XSRF-TOKEN” 下时,Angular 才会识别它。
  • Angular 将始终以专用名称 “X-XSRF-TOKEN” 作为请求头发送 cookie 令牌。
  • 您的应用程序必须能够以 Angular 的专用名称生成 CSRF cookie,并能以 Angular 的专用名称验证 CSRF 请求头。

让我们看看如何做到这一点

public void Configure
(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
  app.Use((context, next) =>
  {
    // return current accessed path
    string path = context.Request.Path.Value;

    if (path.IndexOf("/api/", StringComparison.OrdinalIgnoreCase) != -1)
    {
      var tokens = antiforgery.GetAndStoreTokens(context);
      context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
        new CookieOptions() { HttpOnly = false });
    }

    return next();
  });
}

代码相当简单,它执行以下操作

  • 它首先定义一个中间件,该中间件将对每个请求执行。此中间件将负责生成具有专用名称的 CSRF cookie。
  • 在这个中间件中,它会检查请求的路径。您不需要为所有请求都生成 cookie,只需要为针对 API 的请求生成。从技术上讲,您需要为目标是我们的 API 控制器(即“/api/”)的请求生成此 cookie。
  • 接下来,代码使用 IAntiforgery 服务通过 GetAndStoreTokens 方法生成 CSRF 令牌并将其存储在 cookie 中。我们之前在手动令牌生成部分使用过这种机制。除非您使用 AddForgery 方法(前面提到过)更改了默认的 cookie 名称,否则生成的令牌的默认名称将以 “.AspNetCore.Antiforgery” 开头,正如我们之前所说。
  • 最后,代码读取生成的令牌,并将其存储在当前请求的 cookie 中。它使用专用的 Angular cookie 名称,“XSRF-TOKEN”。

下一步是配置生成的应用程序以读取正确的请求头。如前所述,我们必须保留默认的 Angular CSRF 请求头名称,“X-XSRF-TOKEN”。

services.AddAntiforgery(opts =>
{
  opts.HeaderName = "X-XSRF-TOKEN";

});

现在,运行应用程序,看看它的神奇之处。我们不再收到 400 bad request 错误。当使用 Chrome 开发者工具或 Fiddler 检查请求时,我们可以清楚地看到生成的 cookie 和请求头名称。

Angular 绝对路径问题

如果请求路径是相对路径,上述代码将完美运行

debit(amount: number): Observable<number> {
  // relative path
  let url = `/api/debit?amount=${amount}`;
  return this.request(url);
}

request(url: string): Observable<number> {
  let result: number;
  return this.http.post<number>(url, { });
}

然而,当请求路径是绝对路径时,即使是同一主机,Angular 也不够智能到在请求中包含 CSRF 令牌。您必须手动将其包含在请求中。例如,以下代码将无法工作

credit(amount: number): Observable<number> {
  // absolute path
  let url = this.baseUrl + `api/credit?amount=${amount}`;
  return this.request(url);
}

解决这种情况的办法是手动将请求头添加到请求中,或者使用 HTTP 拦截器自动将其添加到所有请求中。让我们看看如何定义我们的拦截器。我们将从拦截器类本身开始。

import { Injectable } from "@angular/core";
import { HttpInterceptor, HttpXsrfTokenExtractor } from "@angular/common/http";
import { HttpEvent, HttpHandler, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs";

@Injectable()
export class XsrfInterceptor implements HttpInterceptor {
  constructor(private xsrfTokenExtractor: HttpXsrfTokenExtractor) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // load token
    let xsrfToken = this.xsrfTokenExtractor.getToken();

    if (xsrfToken != null) {
      // create a copy of the request and
      // append the XSRF token to the headers list
      const authorizedRequest = req.clone({
        withCredentials: true,
        headers: req.headers.set('X-XSRF-TOKEN', xsrfToken)
      });

      return next.handle(authorizedRequest);
    } else {
      Return next.handle(req);
    }
  }
}

上面的代码很简单,它使用 HttpXsrfTokenExtractor 服务从 cookie 中提取令牌。然后,它复制当前请求,并使用专用名称 “X-XSRF-TOKEN” 附加 CSRF 请求头。最后,它将请求通过管道传递给下一个处理程序。

为了能够使用 HttpXsrfTokenExtractor 服务,您需要在应用的 imports 中包含其模块。

  imports: [
    HttpClientModule,
  ],

最后,您必须将您的拦截器包含在应用程序的可注入对象中。

  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: XsrfInterceptor, multi: true }
  ],

现在您可以运行应用程序,看看它如何为所有请求平稳运行。

值得一提的是,如果您的 Angular 应用包含在一个 Razor CSHTML 文件中,您可以简单地使用常规方式生成 CSRF 令牌

<div>
    <app-root></app-root>
    @Html.AntiForgeryToken()
</div>

在 Angular 代码中,您可以使用类似 jQuery 的方式来获取令牌的值,并使用如下代码将其包含在您的请求中

declare var $: any;

const httpOptions = {
    headers: new HttpHeaders({
      'X-XSRF-Token': $('input[name=__RequestVerificationToken]').val()
    })
  };
this.http.post(url, httpOptions);

摘要

在本文结束时,我想您已经全面了解了 CSRF 攻击的工作原理以及如何保护您的应用程序免受这些攻击。

再次强调,示例的源代码可在 GitHub 上通过以下链接获取

仓库中有五个项目

  • 攻击示例项目,AttackSample.AttackerAppAttackSample.VulnerableApp,展示了对一个易受攻击的应用发起的有效 CSRF 攻击。
  • 安全示例项目,SecureSample.SecureAppSecureSample.AttackerApp,展示了对一个受保护的应用发起的失败的 CSRF 攻击。
  • Angular 项目,SecureSample.AngularApp,这是一个独立的项目。

历史

  • 2021年3月21日:初始版本
© . All rights reserved.