此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

跨站请求伪造(CSRF)

Spring 提供了全面的支持,用于防范跨站请求伪造(CSRF)攻击。 在以下各节中,我们将探讨:spring-doc.cadn.net.cn

本文档的这一部分讨论了 CSRF 保护的一般性主题。 有关基于 ServletWebFlux 的应用程序的 CSRF 保护的具体信息,请参阅相关章节。spring-doc.cadn.net.cn

什么是 CSRF 攻击?

理解 CSRF 攻击的最佳方式是通过一个具体的例子来观察。spring-doc.cadn.net.cn

假设您的银行网站提供一个表单,允许将当前登录用户账户中的资金转账至另一个银行账户。 例如,转账表单可能如下所示:spring-doc.cadn.net.cn

转账表单
<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 请求可能如下所示:spring-doc.cadn.net.cn

传输 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 页面:spring-doc.cadn.net.cn

恶意转账表单
<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仍会随请求一同发送。spring-doc.cadn.net.cn

更糟糕的是,整个过程本可以通过使用 JavaScript 自动完成。 这意味着你甚至不需要点击按钮。 此外,当访问一个遭受XSS 攻击的正常网站时,这种情况同样很容易发生。 那么,我们该如何保护用户免受此类攻击呢?spring-doc.cadn.net.cn

防范 CSRF 攻击

CSRF 攻击之所以可能发生,是因为来自受害者网站的 HTTP 请求与来自攻击者网站的请求完全相同。 这意味着无法拒绝来自恶意网站的请求,同时只允许来自银行网站的请求。 为了防范 CSRF 攻击,我们需要确保请求中包含某些恶意网站无法提供的内容,从而区分这两种请求。spring-doc.cadn.net.cn

Spring 提供了两种机制来防范 CSRF 攻击:spring-doc.cadn.net.cn

这两种保护措施都要求安全方法必须是只读的spring-doc.cadn.net.cn

安全方法必须是只读的

要使任一 CSRF 防护机制生效,应用程序必须确保“安全”的 HTTP 方法是只读的。 这意味着使用 HTTP GETHEADOPTIONSTRACE 方法的请求不应更改应用程序的状态。spring-doc.cadn.net.cn

同步器Tokens模式

防范 CSRF 攻击最主要且最全面的方法是使用同步Tokens模式(Synchronizer Token Pattern)。 该解决方案确保每个 HTTP 请求除了会话 Cookie 外,还必须包含一个称为 CSRF Tokens的安全随机生成值。spring-doc.cadn.net.cn

当提交一个 HTTP 请求时,服务器必须查找预期的 CSRF Tokens,并将其与 HTTP 请求中的实际 CSRF Tokens进行比较。 如果两个值不匹配,则应拒绝该 HTTP 请求。spring-doc.cadn.net.cn

此机制生效的关键在于,实际的 CSRF Tokens应位于 HTTP 请求中浏览器不会自动包含的部分。 例如,要求在 HTTP 参数或 HTTP 头中提供实际的 CSRF Tokens,即可防范 CSRF 攻击。 而要求在 Cookie 中提供实际的 CSRF Tokens则无效,因为 Cookie 会被浏览器自动包含在 HTTP 请求中。spring-doc.cadn.net.cn

我们可以放宽要求,仅对每个会更新应用程序状态的 HTTP 请求强制使用实际的 CSRF Tokens。 为此,我们的应用程序必须确保安全的 HTTP 方法是只读的。 这样做可以提升可用性,因为我们希望允许从外部网站链接到我们的网站。 此外,我们不希望在 HTTP GET 请求中包含随机Tokens,因为这可能导致Tokens泄露。spring-doc.cadn.net.cn

考虑一下当我们使用同步Tokens模式(Synchronizer Token Pattern)时,我们的示例会发生怎样的变化。 假设实际的 CSRF Tokens必须位于名为 _csrf 的 HTTP 参数中。 我们应用程序的转账表单将如下所示:spring-doc.cadn.net.cn

同步器Tokens表单
<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 Tokens。 由于同源策略确保恶意网站无法读取响应,因此外部站点无法获取 CSRF Tokens。spring-doc.cadn.net.cn

对应的用于转账的 HTTP 请求如下所示:spring-doc.cadn.net.cn

同步器Tokens请求
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 Tokens与预期的 CSRF Tokens进行比对时,转账操作将会失败。spring-doc.cadn.net.cn

SameSite 属性

防范CSRF 攻击的一种新兴方法是在 Cookie 上指定SameSite 属性。 服务器在设置 Cookie 时可以指定 SameSite 属性,以表明该 Cookie 在来自外部站点的请求中不应被发送。spring-doc.cadn.net.cn

Spring Security 不直接控制会话 cookie 的创建,因此它不支持 SameSite 属性。 Spring Session 为基于 Servlet 的应用程序提供了对 SameSite 属性的支持。 Spring Framework 的 CookieWebSessionIdResolver 为基于 WebFlux 的应用程序提供了开箱即用的 SameSite 属性支持。spring-doc.cadn.net.cn

一个带有 SameSite 属性的 HTTP 响应头示例如下所示:spring-doc.cadn.net.cn

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

SameSite 属性的有效值包括:spring-doc.cadn.net.cn

考虑一下我们的示例如何使用SameSite属性进行保护。 银行应用程序可以通过在会话 Cookie 上指定SameSite属性来防范 CSRF 攻击。spring-doc.cadn.net.cn

在我们的会话 Cookie 上设置了 SameSite 属性后,浏览器在来自银行网站的请求中仍会继续发送 JSESSIONID Cookie。 然而,当请求来自恶意网站的转账操作时,浏览器将不再发送 JSESSIONID Cookie。 由于来自恶意网站的转账请求中不再包含会话信息,应用程序因此受到保护,免受 CSRF 攻击。spring-doc.cadn.net.cn

在使用 https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-5 属性防范 CSRF 攻击时,有一些重要的注意事项需要了解。spring-doc.cadn.net.cn

SameSite 属性设置为 Strict 可提供更强的防护,但可能会使用户感到困惑。 假设某用户一直登录着托管在 social.example.com 的社交媒体网站。 该用户在 email.example.org 收到一封电子邮件,其中包含指向该社交媒体网站的链接。 如果用户点击该链接,他们理所当然地期望自己已在社交媒体网站上通过身份验证。 然而,如果 SameSite 属性被设为 Strict,则 Cookie 不会被发送,因此用户将无法通过身份验证。spring-doc.cadn.net.cn

另一个显而易见的考虑因素是,为了使 SameSite 属性能够保护用户,浏览器必须支持 SameSite 属性。 大多数现代浏览器确实支持 SameSite 属性。 然而,仍在使用的旧版浏览器可能不支持。spring-doc.cadn.net.cn

因此,我们通常建议将 SameSite 属性作为纵深防御手段,而非仅依赖它来防范 CSRF 攻击。spring-doc.cadn.net.cn

何时使用 CSRF 保护

何时应使用 CSRF 保护? 我们建议对任何可能由普通用户通过浏览器发起的请求都启用 CSRF 保护。 如果您正在创建一个仅由非浏览器客户端使用的服务,则很可能需要禁用 CSRF 保护。spring-doc.cadn.net.cn

CSRF 防护与 JSON

一个常见的问题是:“我是否需要保护由 JavaScript 发起的 JSON 请求?” 简短的回答是:视情况而定。 然而,你必须非常小心,因为存在一些 CSRF 攻击可以影响 JSON 请求。 例如,恶意用户可以通过使用以下表单构造针对 JSON 的 CSRF 攻击spring-doc.cadn.net.cn

使用 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 结构spring-doc.cadn.net.cn

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

如果应用程序未对 Content-Type 请求头进行验证,就会面临此漏洞的威胁。 根据具体配置,即使一个 Spring MVC 应用程序对 Content-Type 进行了验证,攻击者仍可能通过将 URL 后缀修改为以 .json 结尾的方式实施攻击,如下所示:spring-doc.cadn.net.cn

使用 JSON 的 Spring MVC 表单的 CSRF
<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 攻击。spring-doc.cadn.net.cn

例如,考虑一个使用自定义 Cookie 的应用程序,该 Cookie 包含了用于身份验证的全部状态信息(而不是使用 JSESSIONID)。 当发生 CSRF 攻击时,该自定义 Cookie 会随请求一起发送,其发送方式与我们前面示例中 JSESSIONID Cookie 的发送方式相同。 此应用程序容易受到 CSRF 攻击。spring-doc.cadn.net.cn

使用基本身份验证(Basic Authentication)的应用程序也容易受到 CSRF 攻击。 该应用程序之所以存在漏洞,是因为浏览器会自动在任何请求中包含用户名和密码,其方式与我们之前示例中发送 JSESSIONID Cookie 的方式相同。spring-doc.cadn.net.cn

CSRF 注意事项

在实现针对 CSRF 攻击的防护时,有一些特殊事项需要考虑。spring-doc.cadn.net.cn

登录

为了防止伪造登录请求,登录的 HTTP 请求应受到 CSRF 攻击防护。 防止伪造登录请求是必要的,以避免恶意用户读取受害者的敏感信息。 该攻击的实施方式如下:spring-doc.cadn.net.cn

  1. 恶意用户使用其自己的凭据执行 CSRF 登录。 受害者现在已被认证为该恶意用户。spring-doc.cadn.net.cn

  2. 然后,恶意用户诱骗受害者访问被攻破的网站并输入敏感信息。spring-doc.cadn.net.cn

  3. 该信息与恶意用户的账户相关联,因此恶意用户可以使用自己的凭据登录并查看受害者的敏感信息。spring-doc.cadn.net.cn

确保登录 HTTP 请求免受 CSRF 攻击的一个潜在复杂情况是,用户可能会遇到会话超时,从而导致请求被拒绝。 对于那些未预料到登录操作还需要有效会话的用户来说,会话超时会令人感到意外。 更多信息请参阅 CSRF 与会话超时spring-doc.cadn.net.cn

正在退出

为了防止伪造注销请求,注销的 HTTP 请求应受到 CSRF 攻击防护。 防止伪造注销请求是必要的,以免恶意用户读取受害者的敏感信息。 有关该攻击的详细信息,请参阅这篇博客文章spring-doc.cadn.net.cn

确保注销(logout)HTTP 请求免受 CSRF 攻击的一个潜在复杂情况是,用户可能会遇到会话超时,从而导致该请求被拒绝。 对于那些并未预料到自己拥有需要注销的会话的用户来说,会话超时会令人感到意外。 更多信息,请参阅 CSRF 与会话超时spring-doc.cadn.net.cn

跨站请求伪造(CSRF)与会话超时

大多数情况下,预期的 CSRF Tokens存储在会话(session)中。 这意味着一旦会话过期,服务器就找不到预期的 CSRF Tokens,从而拒绝该 HTTP 请求。 有多种方案(每种方案都有其权衡取舍)可用于解决超时问题:spring-doc.cadn.net.cn

  • 缓解超时问题的最佳方法是在表单提交时使用 JavaScript 请求 CSRF Tokens。 然后使用该 CSRF Tokens更新表单并提交。spring-doc.cadn.net.cn

  • 另一种选择是使用一些 JavaScript 代码,在用户会话即将过期时提醒用户。 用户可以点击一个按钮以继续操作并刷新会话。spring-doc.cadn.net.cn

  • 最后,预期的 CSRF Tokens可以存储在 Cookie 中。 这使得预期的 CSRF Tokens的生命周期可以超过会话的有效期。spring-doc.cadn.net.cn

    有人可能会问,为什么默认情况下预期的 CSRF Tokens不存储在 Cookie 中。 这是因为已知存在一些攻击手段,允许其他域设置请求头(例如用于指定 Cookie 的头部)。 这也是 Ruby on Rails 不再在存在 X-Requested-With 头部时跳过 CSRF 检查 的原因。 有关如何实施此类攻击的详细信息,请参见 webappsec.org 上的这个讨论帖。 另一个缺点是,如果移除了状态(即超时机制),一旦Tokens被泄露,你将失去强制使其失效的能力。spring-doc.cadn.net.cn

多部分(文件上传)

保护多部分请求(文件上传)免受 CSRF 攻击会导致一个先有鸡还是先有蛋的问题。 为了防止 CSRF 攻击的发生,必须读取 HTTP 请求的主体以获取实际的 CSRF Tokens。 然而,读取请求主体意味着文件已被上传,这就使得外部网站能够上传文件。spring-doc.cadn.net.cn

使用 multipart/form-data 时,有三种方式可以启用 CSRF 保护:spring-doc.cadn.net.cn

每种选项都有其权衡取舍。spring-doc.cadn.net.cn

在将 Spring Security 的 CSRF 保护与多部分文件上传集成之前,您应首先确保可以在没有 CSRF 保护的情况下进行上传。 有关在 Spring 中使用多部分表单的更多信息,请参阅 Spring 参考文档中的 Multipart Resolver 部分以及 MultipartFilter Javadocspring-doc.cadn.net.cn

包含CSRFTokens在HTTP请求头中

当JavaScript可用时,您可以提交包含CSRFTokens的HTTP请求头以发送multipart请求。 这避免了将Tokens放在URL中,并在CSRF验证之前处理multipart请求体。 这对于具有JavaScript客户端的浏览器应用程序通常是首选的方法。spring-doc.cadn.net.cn

JavaScript 应用程序了解 Servlet 应用程序,以及见AJAX 和 JSON 请求了解 Reactive 应用程序。spring-doc.cadn.net.cn

将 CSRF Tokens放入请求体中

另一个选项是在请求的主体中包含实际的 CSRF Tokens。 通过将 CSRF Tokens放在主体中,主体会在授权之前被读取。 这意味着任何人都可以在您的服务器上放置临时文件。 然而,只有经过授权的用户才能提交由您的应用程序处理的文件。 总体而言,这是一般推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。spring-doc.cadn.net.cn

在 URL 中包含 CSRF Tokens

如果让未授权用户上传临时文件不可接受,并且JavaScript不可用,可以将预期的CSRFTokens作为查询参数包含在表单的action属性中。 这种方法的缺点是查询参数可能会泄漏。 更一般地说,最好将敏感数据放在body或headers中以确保不会泄漏。 您可以在以下链接中找到更多信息: RFC 2616 第15.1.3节 URI 中编码敏感信息.spring-doc.cadn.net.cn

隐藏 HTTP 方法过滤器

某些应用程序可以使用表单参数来覆盖 HTTP 方法。 例如,以下表单可以将 HTTP 方法视为 delete 而不是 postspring-doc.cadn.net.cn

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

重写 HTTP 方法是在一个过滤器中完成的。 该过滤器必须放置在 Spring Security 支持之前。 请注意,重写仅发生在 post 请求上,因此实际上不太可能引发任何真正的问题。 不过,最佳实践仍然是确保将其置于 Spring Security 的过滤器之前。spring-doc.cadn.net.cn