跨站点请求伪造(又名CSRF或XSRF)是最常见的攻击之一,用户被欺骗通过他的浏览器代表他执行一个不需要的操作,在他当前通过身份验证的一个站点中。ASP.Net Core包含一个Antiforgery包,可用于确保您的应用程序免受此特定风险的影响。

简要的CSRF概述

CSRF攻击依赖于以下事实:大多数身份验证系统将在浏览器中存储凭据(例如身份验证cookie),这些凭据会随着对特定网站域/子域的任何请求而自动发送。

攻击者然后可以通过社交媒体,电子邮件和其他方式诱骗用户进入恶意或受感染的站点,他们可以在其中向受攻击或目标站点发送请求。如果用户在目标站点上进行了身份验证,则此请求将包含其凭据,并且该站点将无法将其与合法请求区分开来。例如,黑客可能会向打开恶意网站的用户发送链接。在这个网站中,他们会欺骗用户点击一个按钮,在用户通过身份验证的目标网站上发布隐藏表单。攻击者将伪造这张表格,并将其张贴在一个类似于资金转移网址的URL上,并且包含诸如将一些资金转移到攻击者帐户的数据!图1可视化地描述了CSRF的实际应用。

 

图1:CSRF概述

理解这些请求对攻击者有任何好处是很重要的,他们必须改变应用程序中的某些状态。简单地检索一些数据对他们来说是无用的,就像发送请求并接收到响应的用户及其浏览器一样(即使他们不知道)。当然,攻击的影响取决于具体的应用程序和实际的用户权限,但它可以用来代表用户尝试资金转账或购买,代表他们发送消息,更改电子邮件/密码,如果是管理用户损失甚至会更大。

OWASP维护一个页面,提供针对此次攻击的推荐预防措施,其中包括:

  • 验证请求的来源
  • 在每个请求中包含一个CSRF令牌(这应该是一个加密的伪随机值,因此攻击者本身不能伪造令牌)

可以在OWASP页面上阅读有关此攻击的更多信息以及推荐的技术。开放Web应用安全项目(OWASP)是一个全球性的非营利性慈善组织,致力于提高软件的安全性,将其核心目标定义为“成为推动全球社区安全和安全世界的软件“。您可以在他们的关于页面阅读更多关于它们的信息。

ASP.Net Core Antiforgery如何工作 

ASP.Net Core包含一个名为Antiforgery的包,可用于保护您的网站免受CSRF攻击。 该包实现了OWASP站点推荐的CSRF令牌机制。更具体地说,它实现OWASP备忘单中描述的Double Submit Cookie和Encrypted Token Pattern的混合。这基本上意味着它提供了一个无状态的防御机制,由两个项目(或标记集)组成,这些项目应在Antiforgery包中验证的任何请求中找到:

  • 作为cookie包含的防伪令牌,以伪随机值的形式生成,并使用新的Data Protection API进行加密
  • 一个额外的标记包含作为表单域,标题或cookie。这包括相同的伪随机值,加上来自当前用户身份的附加数据。它也使用Data Protection API加密。

这些令牌将生成服务器端并与HTML文档一起传播到用户的浏览器。当浏览器发送新的请求时,cookie标记将被默认包含,而应用程序需要确保请求标记也包含在内。 如果出现以下情况,请求将被拒绝:

  • 2个令牌中的任何一个都丢失或者格式/加密格式不正确
  • 它们的伪随机值是不同的
  • 嵌入在第二令牌中的用户数据与当前认证的用户不匹配

攻击者无法自己伪造这些令牌。当他们被加密时,他需要在加密令牌之前破解加密。如果处于Web Farm场景中,那么检查Data Protection API如何存储密钥以及如何使用应用程序标识符隔离它们对您来说非常重要。 因为当Web Farm的多个单独实例使用不同的密钥时,它们将无法知晓来自其他实例的秘钥。 所以需要配置Data Protection API,以便实例共享相同的密钥,从而能够相互解密。 有关身份验证cookie的示例,请参阅asp文档

图2:合法请求中的防伪与CSRF攻击请求

此外,无论何时令牌都会由服务器生成并包含在响应中,X-FRAME-OPTIONS标头都包含在值SAMEORIGIN中。 这可以防止浏览器在可能尝试点击劫持攻击的恶意网站内的iframe中呈现页面。以下部分将详细介绍如何在控制器操作中执行Antiforgery验证,以及如何确保两个令牌包含在请求中。

在您的ASP.NET Core应用程序中使用Antiforgery

为项目添加防伪技术

Microsoft.AspNetCore.Antiforgery软件包已作为Microsoft.AspNetCore.Mvc的依赖项包含在内。 同样,通过在Startup.ConfigureServices()方法中调用services.addMvc(),所需的Antiforgery服务会自动在DI容器中注册。

仍然有些情况需要手动将Antiforgery添加到项目中:  

1. 需要覆盖默认Antiforgery选项,则需要手动调用services.AddAntiforgery(opts => {opts setup}),以便可以提供特定的选项设置。 这些选项包括诸如cookie标记的cookie名称以及请求标记的表单字段或标题名称等内容。

2. 在不使用MVC框架的情况下从头开始编写ASP.Net Core应用程序,则需要手动包含Antiforgery包并注册服务。

请记住,这只是在项目中注册Antiforgery并设置选项。 它不会自动启用任何类型的请求验证! 需要按照以下部分手动执行操作。

保护控制器的行为

即使Antiforgery已添加到项目中,令牌也不会自动生成,也不会根据任何请求进行验证,除非启用它们。 本节介绍如何将令牌验证启用为请求处理的一部分,而以下各节介绍如何生成令牌并确保它们包含在浏览器发送的请求中。可以通过依赖注入手动添加一些需要IAntiforgery实例的中间件,然后调用ValidateRequestAsync(httpContext),捕获任何AntiforgeryValidationException并在此情况下返回400。

try
{
    await _antiforgery.ValidateRequestAsync(context);
    await next.Invoke();
}
catch (AntiforgeryValidationException exception)
{
    context.Response.StatusCode = 400;
}

如果使用的是MVC,那么会有Antiforgery特定的授权过滤器。 只需确保控制器操作使用以下属性的组合来保护:

  • [AutoValidateAntiforgeryToken] - 这会在任何“不安全”请求中添加防伪验证,其中不安全意味着请求的方法不是GET,HEAD,TRACE和OPTIONS。 (因为其他HTTP方法是用于改变服务器状态的请求)。 它将应用于全局(作为全局过滤器添加时)或类级别(添加到特定控制器类时)。
  • [ValidateAntiForgeryToken] - 这用于将Antiforgery验证添加到特定的控制器操作或类。 它不考虑请求HTTP方法。 因此,如果添加到类中,它会将验证添加到所有操作,即使是那些具有GET,HEAD,TRACE或OPTIONS方法的验证。
  • [IgnoreAntiforgeryToken] - 在特定操作或控制器中禁用Antiforgery验证。 例如,您可能会在全局或整个控制器类中添加Antiforgery验证,但您可能仍希望忽略特定操作中的验证。

可以混合和匹配这些属性以满足项目需求和团队偏好。 例如:

  • 可以添加AutoValidateAntiforgeryToken作为全局筛选器,并在极少数情况下使用IgnoreAntiforgeryToken,在这种情况下,可能需要为“不安全”请求禁用它。
  • 可以将AutoValidateAntiforgeryToken添加到WebAPI风格的控制器(手动或通过约定),并使用MVC风格的控制器中的ValidateAntiforgeryToken处理许多GET请求,并返回一些不安全的请求,例如PUT和POST操作。

找到最适合项目的方法。 只要确保验证适用于需要的每个操作。现在让我们来看看如何确保令牌包含在浏览器发送的请求中。

在服务器端生成的表单中包含令牌

为了防伪验证成功,需要确保cookie标记和请求标记都包含在将通过验证的请求中。每当使用新的标记助手在Razor视图中生成表单时,请求标记将自动包含为名为__RequestVerificationToken(在添加Antiforgery服务时可以在选项中设置不同的名称)的隐藏字段。例如下面的Razor代码:

<form asp-controller="Foo" asp-action="Bar"><button type="submit">Submit</button>
</form>

将生成以下html:

<form action="/Foo/Bar" method="post"><button type="submit">Submit</button>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8P4n6uxULApNkzyVaa34lxdNGtmIOsdcJ7SYtZiwwTeX9DUiCWhGIndYmXAfTqW0U3sdSpzJ-NMEoQPjxXvx6-1V-5sAonTik5oN9Yd1hej6LmP1XcwnoiQJ2dRAMyhOMIYqbduDdRI1Uxqfd0GszvI">
</form>

只需确保表单包含一些asp- *标记,它会被Razor解释为标记助手,而不仅仅是一个常规表单元素。

  • 如果出于某种原因想要从表单中省略隐藏字段,可以通过向表单元素添加属性asp-antiforgery =“false”来实现。
  • 如果您在Razor视图中使用html助手(如@ Html.BeginForm()),您仍然可以使用@ Html.AntiForgeryToken()手动生成隐藏的输入字段。

这就是您何生成请求标记作为表单中的隐藏字段,然后将其包含在发布的数据中。

怎么样的cookie令牌?
Antiforgery会将请求和Cookie标记作为标记集处理。 当执行IAntiforgery方法GetAndStoreTokens(httpContext)(这是在生成表单隐藏字段时幕后发生的事情)时,它返回包含请求和cookie标记的标记集。 不仅如此,它还将确保:

  • cookie标记作为仅HttpOnly添加到当前的HttpContext响应中。
  • 如果已经为此请求生成了令牌集(即,已经使用相同的httpContext调用了GetAndStoreTokens),它将返回相同的令牌集,而不是再次重新生成它。 这一点很重要,因为它允许页面的多个元素得到发布并通过验证,就像有多个表单一样(因为对于所有表单,cookie都是相同的,如果每个表单都有不同的标记,那么只有带有 与cookie匹配的令牌可以通过验证!)。

许多cookie属性可以通过选项进行更改:

  • Cookie始终设置为仅限http的cookie(所以JavaScript无法访问它)
  • 默认的Cookie名称是为每个应用程序生成的“.AspNetCore.AntiForgery”,后面是应用程序名称的散列。 可以通过在选项中提供CookieName来覆盖它。
  • 默认情况下没有设置cookie域,因此浏览器会采用请求域。 可以通过CookieDomain选项指定一个。
  • Cookie路径默认从当前请求路径基(通常为“/”),但可以通过CookiePath选项强制。
  • 还可以在选项中启用标志RequireSsl,这会将Cookie的Secure标志设置为true。

由于浏览器将随后的请求发送cookie,无论何时发布表单,请求都将包含cookie标记(在cookie中)和请求标记(在表单字段中)。简而言之,只要您以某种方式生成请求令牌,Cookie令牌也会生成并自动添加到响应中。

在AJAX请求中包含令牌

在上一节中,已经看到了如何在表单中使用请求令牌生成隐藏字段。 但是,在将数据作为AJAX请求的一部分发送时,如果没有涉及任何服务器端生成的表单,怎么办?这很大程度上取决于用于构建客户端代码的JavaScript框架。 Angular提供了开箱即用的CSRF支持,现在很常用,即使Antiforgery回购也包含一个示例。 我也将看看Angular-Antiforgery集成,稍后解释如何为简单的jQuery请求手动引入类似的方法。

CSRF - Angular案例

在Angular的情况下,使用他们的$http服务来发送AJAX请求。 如果该服务可以将标记值作为名称为XSRF-TOKEN的cookie找到,那么该服务将自动包含名为X-XSRF-TOKEN的标头。 所以最简单的方法就是玩Angular想要的方式,并创建一些中间件来获取请求令牌,并将其值存储为XSRF-TOKEN cookie。

即使它被添加为cookie,这仍然是请求令牌而不是cookie令牌! 这可能听起来很混乱,所以让我试着澄清一下:

  • 该应用程序将使用cookie令牌将请求令牌和另一个cookie.AspNetCore.Antiforgery.*发回给浏览器一个cookie XSRF-TOKEN。
  • 每当Angular发送一个Ajax请求时,该请求将包含带请求令牌的标头X-XSRF-TOKEN和带Cookie标记的cookie.AspNetCore.Antiforgery.*。
  • Antiforgery验证将确保这两个令牌都是有效的并且共享相同的秘钥等。

图3:使用Angular的CSRF令牌

 由于请求令牌的默认标头名称为RequestVerificationToken,因此我们需要对其进行更改并确保Antiforgery在标头中搜索名称为X-XSRF-TOKEN的请求令牌。 只需手动添加Antiforgery并在ConfigureServices方法中设置选项:

services.AddAntiforgery(opts => opts.HeaderName = "X-XSRF-Token");

现在需要确保生成令牌并将请求令牌包含在名为XSRF-TOKEN的cookie中,以便Angular $http服务可以读取它并将其作为头部包含它。

  • 这不能仅仅是一个http cookie,因为Angular代码需要读取cookie值,以便在随后的请求中包含它作为头文件!

每次我们生成一个完整的html文档时,我们都会对此感兴趣,所以我们可以创建一个新的结果过滤器,如果结果是ViewResult,基本上会这样做:

public class AngularAntiforgeryCookieResultFilter: ResultFilterAttribute
{
    private IAntiforgery antiforgery;
    public AngularAntiforgeryCookieResultFilter(IAntiforgery antiforgery)
    {
        this.antiforgery = antiforgery;
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is ViewResult)
        {
            var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
            context.HttpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
        }
    }
}

剩下的唯一一点是将其配置为全局过滤器。 这种方式每次呈现一个完整的html文档时,响应也会包括cookie令牌和请求令牌的cookie。 将在ConfigureServices方法中再次执行此操作:

services.AddAntiforgery(opts => opts.HeaderName = "X-XSRF-Token");
services.AddMvc(opts =>
{
    opts.Filters.AddService(typeof(AngularAntiforgeryCookieResultFilter));
});
services.AddTransient< AngularAntiforgeryCookieResultFilter >();

就是这样,现在的Angular代码可以使用$http服务,并且令牌将包含在请求中。 可以在GitHub中使用简单的TODO Angular应用程序来检查示例。

CSRF - 使用jQuery的手动案例

如果使用的是Angular以外的框架(或根本没有框架),则处理方式标记可能会有所不同。 但是每种情况下的基本原则都是相同的,因为始终需要确保:

  • 令牌是服务器端生成的。
  • 请求令牌可用于客户端JavaScript代码。
  • 客户端JavaScript代码将请求标记包含为标题或表单字段。

假设客户端代码使用jQuery发送AJAX请求。 由于已经创建了一个包含请求标记作为XSRF-TOKEN cookie的结果过滤器,并且配置了Antiforgery以在名为X-XSRF-TOKEN的标头中查找请求标记,所以我们可以重复使用相同的方法。

需要从Cookie获取请求令牌并将其包含在您的请求中(如果使用诸如$.ajaxSetup()之类的内容的请注意,令牌将包含在任何请求中,包括来自使用jQuery的第三方代码的请求):

var token = readCookie('XSRF-TOKEN');
$.ajax({
    url: '/api/todos',
    method: 'PUT',
    data: JSON.stringify(todo),
    contentType: 'application/json',
    headers: { 'X-XSRF-TOKEN': token },
    success: onSuccess
});

readCookie可以是jQuery cookies插件,也可以是自己的用于读取cookie值(从Stack Overflow获取)的实用工具:

function readCookie(name) {
    name += '=';
    for (var ca = document.cookie.split(/;\s*/), i = ca.length - 1; i >= 0; i--)
        if (!ca[i].indexOf(name))
            return ca[i].replace(name, '');
}

但是只是使用这些cookie和标题名称,因为之前为Angular设置了这种服务器。 如果没有使用Angular,可以使用任何喜欢的名字作为cookie的请求标记(只要添加该cookie的中间件使用相同的名称)和默认标题名称(只要没有指定 选项中的不同):

var token = readCookie('AnyNameYouWant');
$.ajax({
    …
    headers: { 'RequestVerificationToken': token },
    …
});

事实上,不一定需要使用Cookie来将请求令牌传播到JavaScript代码。 只要能够保证令牌包含在AJAX请求中,那么验证就可以工作。 例如,可以在Razor布局中呈现一个内嵌脚本,该脚本将请求令牌添加到某个变量中,稍后,由执行jQuery AJAX请求的JavaScript代码使用该脚本:

@*In your layout*@
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery antiforgery;
@{ 
    var antiforgeryRequestToken = antiforgery.GetAndStoreTokens(Context).RequestToken;
}
<script>
    //Render the token in a property readable from your client JavaScript
    app.antiforgeryToken = @Json.Serialize(antiforgeryRequestToken);
</script>
//In your client JavaScript
$.ajax({
    …
    headers: { 'RequestVerificationToken': app.antiforgeryToken },
    …
});

验证请求源

可能已经注意到OWASP推荐了另一种保护措施,验证请求的来源。 他们推荐的验证方式是:

1.获取请求源(通过查看Origin和Referer头文件)

2.获取目标来源(查看主机和X-Forwarded-Host标头)

3.验证请求和目标来源是否相同,或请求来源是否包含在白名单中的其他来源列表中。

可以实现一个IAuthorizeFilter,通过这些步骤处理所有不安全的请求(使用除GET,HEAD,TRACE和OPTIONS以外的方法的任何请求),并且在验证失败时将结果设置为400:

context.Result = new BadRequestResult();

编写这样一个过滤器并将其包含为全局过滤器不太困难。 如果想查看包含白名单额外来源选项的示例,请查看GitHub中的示例项目。

结论

安全性是任何应用的关键方面。 了解不同类型的攻击以及如何防范攻击,本身就是一门艺术。 幸运的是,通过成熟的框架简化了整个安全过程,从而提供更多更好的安全功能。当谈到特定的CSRF攻击时,ASP.Net Core提供了保护应用程序的工具,但仍需要一些(最少的)努力才能正确配置和设置。可以在GitHub上找到一个带有Antiforgery验证的示例项目。

posted on 2018-03-08 11:09  Zhe.Song  阅读(5877)  评论(0编辑  收藏  举报