跨站请求伪造 (CSRF)

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

本部分文档讨论了 CSRF 防护的通用主题。有关基于ServletWebFlux 的应用程序的 CSRF 防护的具体信息,请参见相关章节。

什么是 CSRF 攻击?

理解 CSRF 攻击的最佳方法是查看一个具体的示例。

假设您的银行网站提供了一个表单,允许将资金从当前登录的用户转移到另一个银行账户。例如,转账表单可能如下所示:

转账表单
<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 请求可能如下所示:

转账 HTTP 请求
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 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>

您想赢钱,因此单击提交按钮。在此过程中,您无意中将 100 美元转账给了恶意用户。之所以会发生这种情况,是因为恶意网站无法查看您的 Cookie,但与您的银行关联的 Cookie 仍然会随请求一起发送。

更糟糕的是,整个过程都可以通过使用 JavaScript 自动化。这意味着您甚至不需要单击按钮。此外,在访问受到XSS 攻击 的诚实网站时,也可能同样容易发生这种情况。那么我们如何保护用户免受此类攻击呢?

防御 CSRF 攻击

CSRF 攻击之所以可能发生,是因为来自受害者网站的 HTTP 请求和来自攻击者网站的请求完全相同。这意味着无法拒绝来自恶意网站的请求,而只能允许来自银行网站的请求。为了防御 CSRF 攻击,我们需要确保请求中包含一些恶意站点无法提供的内容,以便我们能够区分这两个请求。

Spring 提供了两种防御 CSRF 攻击的机制:

两种防护措施都需要安全方法为只读

安全方法必须是只读的

为了任何一种防御 CSRF 的方法都能生效,应用程序必须确保"安全" HTTP 方法是只读的。这意味着使用 HTTP `GET`、`HEAD`、`OPTIONS` 和 `TRACE` 方法的请求不应更改应用程序的状态。

同步标记模式

防御 CSRF 攻击的主要且最全面的方法是使用同步标记模式。此解决方案旨在确保每个 HTTP 请求除了我们的会话 Cookie 之外,还需要在 HTTP 请求中提供一个安全生成的随机值,称为 CSRF 令牌。

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

此方法有效的主要原因是,实际的 CSRF 令牌应该位于浏览器不会自动包含的 HTTP 请求部分。例如,在 HTTP 参数或 HTTP 标头中要求实际的 CSRF 令牌将防御 CSRF 攻击。在 Cookie 中要求实际的 CSRF 令牌无效,因为 Cookie 会由浏览器自动包含在 HTTP 请求中。

我们可以放宽预期,只要求更新应用程序状态的每个 HTTP 请求都具有实际的 CSRF 令牌。为此,我们的应用程序必须确保安全 HTTP 方法是只读的。这提高了可用性,因为我们希望允许从外部站点链接到我们的网站。此外,我们不想在 HTTP GET 中包含随机令牌,因为这可能会导致令牌泄露。

考虑一下当我们使用同步令牌模式时,我们的示例会如何变化。假设实际的CSRF令牌需要位于名为_csrf的HTTP参数中。我们的应用程序的转账表单将如下所示:

同步令牌表单
<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>

该表单现在包含一个隐藏的输入,其值为CSRF令牌。外部站点无法读取CSRF令牌,因为同源策略确保恶意站点无法读取响应。

相应的转账HTTP请求如下所示:

同步令牌请求
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上指定SameSite属性。服务器可以在设置cookie时指定SameSite属性,以指示当来自外部站点时不应发送cookie。

Spring Security不直接控制会话cookie的创建,因此它不支持SameSite属性。Spring Session在基于servlet的应用程序中提供对SameSite属性的支持。Spring Framework的CookieWebSessionIdResolver在基于WebFlux的应用程序中提供开箱即用的SameSite属性支持。

带有SameSite属性的HTTP响应头示例可能如下所示:

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

SameSite属性的有效值为:

  • Strict:指定此属性时,任何来自同站点的请求都包含cookie。否则,cookie不包含在HTTP请求中。

  • Lax:指定此属性时,当来自同站点或请求来自顶级导航且方法为只读时,会发送cookie。否则,cookie不包含在HTTP请求中。

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

在我们的会话cookie上设置SameSite属性后,浏览器会继续将JSESSIONID cookie与来自银行网站的请求一起发送。但是,浏览器不再将JSESSIONID cookie与来自恶意网站的转账请求一起发送。由于来自恶意网站的转账请求中不再存在会话,因此应用程序可以免受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请求吗?”简短的回答是:这取决于具体情况。但是,您必须非常小心,因为存在可能影响JSON请求的CSRF漏洞。例如,恶意用户可以使用以下表单创建一个使用JSON的CSRF

使用JSON的CSRF表单
<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结构:

使用JSON的CSRF请求
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

如果应用程序没有验证Content-Type头,它将容易受到此漏洞的攻击。根据设置情况,通过将URL后缀更新为以.json结尾,即使是验证Content-Type的Spring MVC应用程序也可能仍然会被利用,如下所示:

使用JSON的CSRF Spring MVC表单
<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和无状态浏览器应用程序

如果我的应用程序是无状态的怎么办?这并不一定意味着您受到保护。事实上,如果用户不需要为给定的请求在Web浏览器中执行任何操作,他们可能仍然容易受到CSRF攻击。

例如,考虑一个使用自定义cookie的应用程序,该cookie在其内包含所有用于身份验证的状态(而不是JSESSIONID)。当进行CSRF攻击时,自定义cookie将与请求一起以与我们前面示例中发送JSESSIONID cookie相同的方式发送。此应用程序容易受到CSRF攻击。

使用基本身份验证的应用程序也容易受到CSRF攻击。该应用程序容易受到攻击,因为浏览器会自动将用户名和密码包含在任何请求中,其方式与我们前面示例中发送JSESSIONID cookie的方式相同。

CSRF注意事项

在实施针对CSRF攻击的保护时,需要考虑一些特殊事项。

登录

为了防止伪造登录请求,登录HTTP请求应受到CSRF攻击的保护。防止伪造登录请求对于防止恶意用户读取受害者的敏感信息是必要的。攻击过程如下:

  1. 恶意用户使用恶意用户的凭据执行CSRF登录。受害者现在已以恶意用户的身份进行身份验证。

  2. 然后,恶意用户诱骗受害者访问受损的网站并输入敏感信息。

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

确保登录HTTP请求受到CSRF攻击保护的一个可能的并发症是,用户可能会遇到会话超时,导致请求被拒绝。会话超时对于不期望需要会话才能登录的用户来说是令人惊讶的。有关更多信息,请参阅CSRF和会话超时

注销

为了防止伪造注销请求,注销HTTP请求应受到CSRF攻击的保护。防止伪造注销请求对于防止恶意用户读取受害者的敏感信息是必要的。有关攻击的详细信息,请参见这篇博客文章

确保注销HTTP请求受到CSRF攻击保护的一个可能的并发症是,用户可能会遇到会话超时,导致请求被拒绝。会话超时对于不期望需要会话才能注销的用户来说是令人惊讶的。有关更多信息,请参见CSRF和会话超时

CSRF和会话超时

通常情况下,预期的CSRF令牌存储在会话中。这意味着,一旦会话过期,服务器就找不到预期的CSRF令牌并拒绝HTTP请求。有很多解决超时的方法(每种方法都有权衡取舍):

  • 减轻超时的最佳方法是使用JavaScript在表单提交时请求CSRF令牌。然后使用CSRF令牌更新表单并提交。

  • 另一个选项是使用一些JavaScript让用户知道他们的会话即将过期。用户可以单击一个按钮继续并刷新会话。

  • 最后,预期的CSRF令牌可以存储在cookie中。这使得预期的CSRF令牌可以比会话存活更长时间。

    有人可能会问,为什么预期的CSRF令牌不是默认存储在cookie中。这是因为已知存在一些漏洞,其中标头(例如,指定cookie的标头)可以由另一个域设置。这就是Ruby on Rails不再跳过存在X-Requested-With标头时的CSRF检查的原因。有关如何执行此漏洞利用的详细信息,请参见这个webappsec.org主题。另一个缺点是,通过删除状态(即超时),您将失去在令牌被破坏时强制使其无效的能力。

多部分(文件上传)

保护多部分请求(文件上传)免受CSRF攻击会导致先有鸡还是先有蛋的问题。为了防止发生CSRF攻击,必须读取HTTP请求的主体以获取实际的CSRF令牌。但是,读取主体意味着文件已上传,这意味着外部站点可以上传文件。

将CSRF保护与multipart/form-data一起使用有两种选择:

每个选项都有其权衡取舍。

在将Spring Security的CSRF保护与multipart文件上传集成之前,您应该首先确保可以在没有CSRF保护的情况下进行上传。有关使用Spring的多部分表单的更多信息,请参见Spring参考的1.1.11. 多部分解析器部分和MultipartFilter Javadoc

将CSRF令牌放在主体中

第一种选择是将实际的CSRF令牌包含在请求的主体中。通过将CSRF令牌放在主体中,在执行授权之前读取主体。这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。

将CSRF令牌包含在URL中

如果不允许未授权用户上传临时文件,另一种方法是在表单的 action 属性中包含预期的 CSRF 令牌作为查询参数。这种方法的缺点是查询参数可能会泄露。更一般地说,最佳实践是将敏感数据放在正文或标头中,以确保不会泄露。您可以在RFC 2616 第 15.1.3 节“在 URI 中编码敏感信息”中找到更多信息。

HiddenHttpMethodFilter

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

CSRF 隐藏 HTTP 方法表单
<form action="/process"
	method="post">
	<!-- ... -->
	<input type="hidden"
		name="_method"
		value="delete"/>
</form>

HTTP 方法的覆盖发生在过滤器中。该过滤器必须放在 Spring Security 的支持之前。请注意,覆盖仅发生在post上,因此实际上不太可能导致任何实际问题。但是,最佳实践仍然是确保将其放在 Spring Security 的过滤器之前。