Cross Site Request Forgery (CSRF)

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

Spring 提供了对 跨站请求伪造(CSRF) 攻击的全面支持。在下面的章节中,我们将探讨。

这一部分文档讨论的是CSRF保护的一般主题。有关基于 servletWebFlux 的应用程序的CSRF保护的具体信息,请参见相关章节。

什么是CSRF攻击?

了解CSRF攻击的最好方法是看一个具体的例子。

假设你的银行网站提供了一个表单(form),允许将当前登录的用户的钱转到另一个银行账户。例如,这个转账表格可能看起来像下面这样。

Transfer form
<form method="post"
	action="/transfer">
<input type="text"
	name="amount"/>
<input type="text"
	name="routingNumber"/>
<input type="text"
	name="account"/>
<input type="submit"
	value="Transfer"/>
</form>

相应的HTTP请求可能看起来下面这样。

Transfer HTTP request
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

现在假装你登录了你的银行网站,然后在没有注销的情况下,访问一个邪恶的网站。这个“邪恶的网站”包含一个HTML页面,上面有以下表单(form)。

Evil transfer form
<form method="post"
	action="https://bank.example.com/transfer">
<input type="hidden"
	name="amount"
	value="100.00"/>
<input type="hidden"
	name="routingNumber"
	value="evilsRoutingNumber"/>
<input type="hidden"
	name="account"
	value="evilsAccountNumber"/>
<input type="submit"
	value="Win Money!"/>
</form>

你喜欢赢钱(Win Money),所以你点击了提交按钮。在这个过程中,你无意中把100美元转给了一个恶意的用户。发生这种情况的原因是,虽然“邪恶网站”看不到你的cookie,但与你的银行相关的cookie仍然与请求一起被发送。

更糟糕的是,这整个过程本来可以通过使用JavaScript自动完成。这意味着你甚至不需要点击这个按钮。此外,在访问一个遭受 XSS攻击 的“诚实网站”时,它也可能很容易发生。那么,我们如何保护我们的用户免受此类攻击?

防范CSRF攻击

CSRF攻击之所以可能,是因为来自受害者网站的HTTP请求和来自攻击者网站的请求是完全相同的。这意味着没有办法拒绝来自“邪恶网站”的请求而只允许来自银行网站的请求。为了防止CSRF攻击,我们需要确保请求中存在“邪恶网站”无法提供的东西,这样我们就可以区分这两个请求。

Spring提供了两种机制来防止CSRF攻击。

这两种保护措施都要求 Safe Method 必须是幂等的

Safe Method 必须是幂等的

为了使对CSRF的保护发挥作用,应用程序必须确保 安全的HTTP请求方法必须是幂等的 的。这意味着使用 HTTP GETHEADOPTIONSTRACE 方法的请求不应该改变应用程序的状态。

同步令牌(Synchronizer Token)模式

防止CSRF攻击的最主要和最全面的方法是使用 Synchronizer Token 模式。这个解决方案是确保每个HTTP请求除了需要我们的会话cookie外,还需要在HTTP请求中出现一个被称为CSRF令牌的安全随机生成值。

当一个HTTP请求被提交时,服务器必须查找预期的CSRF令牌,并将其与HTTP请求中的实际CSRF令牌进行比较。如果数值不匹配,HTTP请求应被拒绝。

这个工作的关键是,实际的CSRF令牌应该在HTTP请求的某个部分,而不是由浏览器自动包含。例如,在HTTP参数或HTTP header 中要求实际的CSRF令牌可以防止CSRF攻击。在cookie中要求实际的CSRF令牌不起作用,因为cookie会被浏览器自动包含在HTTP请求中。

我们可以放宽预期,只要求每个更新应用程序状态的HTTP请求提供实际的CSRF令牌。要做到这一点,我们的应用程序必须确保 Safe Method 必须是幂等的。这提高了可用性,因为我们希望允许从外部网站链接到我们的网站。此外,我们不希望在HTTP GET中包含随机令牌(Token),因为这可能导致令牌被泄露。

考虑一下当我们使用 Synchronizer Token 模式时,我们的例子会有什么变化。假设实际的CSRF令牌被要求放在一个名为 _csrf 的HTTP参数中。我们的应用程序的传输形式将看起来像下面一样。

Synchronizer Token Form
<form method="post"
	action="/transfer">
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
	name="amount"/>
<input type="text"
	name="routingNumber"/>
<input type="hidden"
	name="account"/>
<input type="submit"
	value="Transfer"/>
</form>

表单现在包含一个隐藏的 input,其中有CSRF令牌的值。外部网站无法读取CSRF令牌,因为相同的起源策略确保“邪恶网站”无法读取响应。

相应的转移资金的HTTP请求看起来是这样的。

Synchronizer Token request
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721

你会注意到,现在的HTTP请求包含了 _csrf 参数的安全随机值。“邪恶网站”将无法为 _csrf 参数提供正确的值(必须在“邪恶网站”上明确提供),当服务器将实际的CSRF令牌与预期的CSRF令牌进行比较时,传输将会失败。

SameSite 属性

防止 CSRF攻击 的一个新方法是在cookie上指定 the SameSite 属性。服务器可以在设置cookie时指定 SameSite 属性,以表明来自外部网站的cookie不应该被发送。

Spring Security 不直接控制session cookie的创建,所以它不提供对 SameSite 属性的支持。 Spring Session 在基于Servlet的应用程序中提供了对 SameSite 属性的支持。Spring Framework 的 CookieWebSessionIdResolver 在基于WebFlux的应用程序中提供了对 SameSite 属性的开箱支持。

一个例子,带有 SameSite 属性的HTTP响应头可能看起来像下面这样。

SameSite HTTP response
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax

SameSite 属性的有效值如下。

  • Strict: 当指定时,任何来自 同一站点 的请求都包括该cookie。否则,cookie不包括在HTTP请求中。

  • Lax: 当指定时,当来自 同一站点 或请求来自顶级导航且 Safe Method 必须是幂等的 时,将发送cookie。否则,cookie不包括在HTTP请求中。

关于 SameSite 属性的详细内容可以参考: Cookie 的 SameSite 属性

考虑一下我们的例子如何使用 SameSite 属性进行保护。银行应用程序可以通过在会话cookie上指定 SameSite 属性来防止CSRF。

在我们的 Session cookie 上设置了 SameSite 属性后,浏览器会在来自银行网站的请求中继续发送 JESSIONID cookie。然而,在来自“邪恶网站”的传输请求中,浏览器不再发送 JESSIONID cookie。由于session 不再出现在来自“邪恶网站”的传输请求中,应用程序被保护免受CSRF攻击。

在使用 SameSite 属性保护CSRF攻击时,有一些重要的 注意事项 需要注意。

SameSite 属性设置为 Strict 提供了更强的防御,但会使用户感到困惑。考虑到一个用户一直在登录一个托管在 social.example.com 的社交媒体网站。该用户在 email.example.org ,收到一封电子邮件,其中包括一个指向该社交媒体网站的链接。如果用户点击了这个链接,他们理所当然地期望被认证到该社交媒体网站。然而,如果 SameSite 属性是 Strict 的,cookie将不会被发送,因此用户将不会被认证。

我们可以通过实现 gh-7537 来改善 SameSite 对CSRF攻击的保护和实用性。

另一个明显的考虑是,为了使 SameSite 属性能够保护用户,浏览器必须支持 SameSite 属性。大多数现代浏览器确实 支持 SameSite 属性。然而,仍在使用的旧版浏览器可能不支持。

由于这个原因,我们通常建议将 SameSite 属性作为深度防御,而不是唯一的保护措施来防止CSRF攻击。

何时使用CSRF保护

什么时候应该使用CSRF保护?我们的建议是,对任何可能被正常用户的浏览器处理的请求使用CSRF保护。如果你正在创建一个只被非浏览器客户端使用的服务,你可能想禁用CSRF保护。

CSRF保护和JSON

一个常见的问题是 "我需要保护由JavaScript发出的JSON请求吗?" 简短的回答是:看情况。然而,你必须非常小心,因为有一些CSRF漏洞可以影响JSON请求。例如,一个恶意的用户可以 通过使用以下形式用JSON创建一个CSRF

CSRF with JSON form
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
	<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
	<input type="submit"
		value="Win Money!"/>
</form>

这将产生以下JSON结构。

CSRF with JSON request
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

如果一个应用程序没有验证 Content-Type 头,它就会暴露在这个漏洞中。根据设置,一个验证了 Content-Type 的 Spring MVC 应用程序仍然可以通过修改URL后缀为 .json 而被利用,如下所示。

CSRF with JSON Spring MVC form
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
	<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
	<input type="submit"
		value="Win Money!"/>
</form>

CSRF和无状态浏览器应用

如果我的应用程序是无状态的呢?这并不一定意味着你得到了保护。事实上,如果用户不需要在网络浏览器中对某一请求进行任何操作,他们很可能仍然容易受到CSRF攻击。

例如,考虑一个使用自定义cookie的应用程序,该cookie中包含了所有用于验证的状态(而不是JSESSIONID)。当CSRF攻击发生时,自定义cookie与请求一起被发送,其方式与我们前面的例子中的JSESSIONID cookie被发送的方式相同。这个应用程序很容易受到CSRF攻击。

使用基本认证的(basic authentication)应用程序也容易受到CSRF攻击。由于浏览器在任何请求中都会自动包含用户名和密码,与我们之前的例子中发送JSESSIONID cookie的方式相同,所以该应用程序容易受到攻击。

CSRF的考虑因素

在实施对CSRF攻击的保护时,有一些特殊的考虑因素需要考虑。

登录

为了防止 伪造登录请求,应该保护登录的HTTP请求免受CSRF攻击。防止伪造登录请求是必要的,这样恶意的用户就不能读取受害者的敏感信息。攻击的方式如下。

  1. 恶意用户用“恶意用户的凭证”进行CSRF登录。受害者现在被认证为恶意用户。

  2. 恶意用户然后欺骗受害者访问被攻击的网站并输入敏感信息。

  3. 这些信息与恶意用户的账户相关联,因此恶意用户可以用他们自己的凭证登录并查看受害者的敏感信息。

确保登录的HTTP请求免受CSRF攻击的一个可能的复杂情况是,用户可能会遇到会话超时的情况,导致请求被拒绝。会话超时对于那些不期望需要会话来登录的用户来说是令人惊讶的。欲了解更多信息,请参考CSRF 和会话(Session)超时

退出登录

为了防止伪造的注销请求,注销的HTTP请求应该被保护起来,以防止CSRF攻击。防止伪造注销请求是必要的,这样恶意的用户就不能读取受害者的敏感信息。关于攻击的细节,请看 这篇博文

要确保注销HTTP请求受到CSRF攻击的保护,一个可能的复杂情况是,用户可能会遇到会话超时的情况,导致请求被拒绝。会话超时对于那些不期望有会话来注销的用户来说是令人惊讶的。欲了解更多信息,请参阅 CSRF 和会话(Session)超时

CSRF 和会话(Session)超时

更多时候,预期的CSRF令牌被存储在会话中。这意味着,一旦会话过期,服务器就找不到预期的CSRF令牌而拒绝HTTP请求。有许多选项(每个选项都有交换条件)来解决超时问题。

  • 缓解超时的最好方法是使用JavaScript在表单提交时请求一个CSRF令牌。然后用CSRF令牌更新表单并提交。

  • 另一个选择是有一些JavaScript,让用户知道他们的会话即将到期。用户可以点击一个按钮来继续并刷新会话。

  • 最后,预期的CSRF令牌可以存储在一个cookie中。这可以让预期的CSRF令牌在会话中失效。

    有人可能会问,为什么预期的CSRF令牌默认不存储在cookie中。这是因为有一些已知的漏洞,在这些漏洞中,header 信息(例如,用于指定cookie)可以由另一个域来设置。这与Ruby on Rails X-Requested-With header出现时不再跳过CSRF检查的原因相同。关于如何执行该漏洞的细节,请参见 webappsec.org 的这个文章。另一个缺点是,通过移除状态(即超时),你就失去了在令牌被泄露时强行使其失效的能力。

Multipart (文件上传)

保护 Multipart 请求(文件上传)免受CSRF攻击会导致一个 鸡或蛋 的问题。为了防止CSRF攻击的发生,必须读取HTTP请求的主体以获得实际的CSRF令牌。然而,读取正文意味着文件被上传,这意味着一个外部网站可以上传文件。

有两种方法可以使用CSRF保护 multipart/form-data

每种选择都有其利弊得失。

在你将 Spring Security 的 CSRF 保护与 multipart 文件上传整合之前,你应该首先确保你可以在没有CSRF保护的情况下进行上传。关于在 Spring 中使用 multipart form 的更多信息,请参见Spring参考资料的 1.1.11. Multipart Resolver 部分和 MultipartFilter Javadoc

在请求体中放置CSRF令牌

第一个选项是将实际的CSRF令牌包含在请求的body中。通过将CSRF令牌放在请求体中,在进行授权之前就会读取请求体。这意味着,任何人都可以在你的服务器上放置临时文件。然而,只有经过授权的用户可以提交一个由你的应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件的上传对大多数服务器的影响应该是可以忽略不计的。

在URL中放置CSRF令牌

如果让未经授权的用户上传临时文件是不可接受的,另一种方法是将预期的CSRF令牌作为查询参数包含在表单的action属性中。这种方法的缺点是查询参数可能被泄露。更普遍的做法是,将敏感数据放在 body 或 header 中,以确保其不被泄露,这是最佳做法。你可以在 RFC 2616第15.1.3节 中找到更多的信息,在URI中编码敏感信息。

HiddenHttpMethodFilter

一些应用程序可以使用表单参数来覆盖HTTP方法。例如,下面的表单可以将HTTP方法视为 delete 而不是 post

CSRF Hidden HTTP Method Form
<form action="/process"
	method="post">
	<!-- ... -->
	<input type="hidden"
		name="_method"
		value="delete"/>
</form>

重写HTTP方法发生在一个过滤器中。该过滤器必须放在 Spring Security 的支持之前。请注意,重写只发生在 post 上,所以这实际上不太可能造成任何实际问题。然而,最好的做法还是确保它被放在 Spring Security 的过滤器之前。