Cross Site Request Forgery (CSRF)

Spring 为抵御 Cross Site Request Forgery (CSRF)攻击提供了全面的支持。在以下部分中,我们探讨:

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

What is a CSRF Attack?

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

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

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 页面,其中包含以下表单:

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>

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

更糟糕的是,整个过程可以通过使用 JavaScript 自动化。这意味着你甚至不需要点击按钮。此外,当访问成为 XSS attack受害者的诚实站点时,也可能发生这种情况。那么我们如何保护我们的用户免受此类攻击?

Protecting Against CSRF Attacks

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

Spring 提供两种机制来防范 CSRF 攻击:

两种保护都需要 Safe Methods be Read-only

Safe Methods Must be Read-only

要让 either protection对抗 CSRF 起作用,应用程序必须确保 "safe" HTTP methods are read-only。这意味着使用 HTTP GETHEAD、`OPTIONS`和 `TRACE`方法的请求不应改变应用程序的状态。

Synchronizer Token Pattern

抵御 CSRF 攻击的主要且最全面方式是使用 Synchronizer Token Pattern。此解决方案是为了确保除了我们的会话 Cookie 之外,每个 HTTP 请求都需要将一个称为 CSRF 令牌的安全随机生成值包含在 HTTP 请求中。

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

此功能的关键在于,实际的 CRSF 令牌应位于 HTTP 请求中不会自动包含在内的部分中。例如,在 HTTP 参数或 HTTP 标头中要求实际 CRSF 令牌将保护免受 CRSF 攻击。在 cookie 中要求实际的 CRSF 令牌不起作用,因为浏览器会自动在 HTTP 请求中包含 cookie。

我们可以放宽要求,仅对更新应用程序状态的每个 HTTP 请求要求实际的 CRSF 令牌。为此,我们的应用程序必须确保 safe HTTP methods are read-only。这提高了可用性,因为我们希望允许从外部站点链接到我们的网站。此外,我们不想在 HTTP GET 中包含随机令牌,因为这会导致令牌泄露。

考虑当我们使用 Synchronizer Token Pattern 时,our example 会如何改变。假设需要实际的 CRSF 令牌才能输入一个名为 _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>

该表单现在包含一个使用 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 Attribute

一种新兴的抵御 CSRF Attacks的方式是在 Cookie 中指定 SameSite Attribute。设置 Cookie 时,服务器可以指定 `SameSite`属性,以指示不应从外部站点发送 Cookie。

Spring Security 不会直接控制会话 Cookie 的创建,因此它不支持 SameSite 属性。 Spring Session为基于 servlet 的应用程序提供了 SameSite`属性的支持。Spring 框架的 `CookieWebSessionIdResolver为基于 WebFlux 的应用程序提供了对 `SameSite`属性的开箱即用支持。

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

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

SameSite 属性的有效值为:

  • Strict:当指明后,来自 same-site的所有请求均包含 Cookie。否则,将不会在 HTTP 请求中包含 Cookie。

  • Lax:当指定时,在来自 same-site时发送 Cookie 或当请求来自顶级导航和method is read-only时发送 Cookie。否则,将不会在 HTTP 请求中包含 Cookie。

考虑如何使用 SameSite 属性保护 our example。银行应用程序可以通过在会话 cookie 中指定 SameSite 属性,从而保护自己免遭 CRSF 攻击。

使用 SameSite 属性设置我们的会话 cookie 后,浏览器继续使用来自银行网站的请求发送 JSESSIONID cookie。但是,浏览器不再使用来自恶意网站的转账请求发送 JSESSIONID cookie。由于会话不再出现在来自恶意网站的转账请求中,因此可以保护应用程序免遭 CRSF 攻击。

在使用 `SameSite`属性来抵御 CSRF 攻击时,有一些重要的 considerations需要注意。

SameSite`属性设置为 `Strict`提供了更强的防御,但可能会迷惑用户。考虑一下一个用户始终登录到托管在 [role="bare"][role="bare"]https://social.example.com的社交媒体网站上。该用户在 [role="bare"][role="bare"]https://email.example.org收到一封电子邮件,其中包含到社交媒体网站的链接。如果用户点击此链接,他们将有权被社交媒体网站验证身份。但是,如果 `SameSite`属性为 `Strict,则不会发送 Cookie,因此不会验证用户身份。

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

另一个显而易见的考虑因素是,为了让 `SameSite`属性保护用户,浏览器必须支持 `SameSite`属性。大多数现代浏览器确实 support the SameSite attribute。然而,仍在使用的旧浏览器可能不支持。

因此,我们通常建议使用 SameSite 属性作为纵深防御,而不是对抗 CSRF 攻击的唯一保护措施。

When to use CSRF protection

您应该在何时使用 CSRF 保护?我们的建议是,对所有可能由浏览器为普通用户处理的请求使用 CSRF 保护。如果您正在创建一个仅供非浏览器客户端使用的服务,那么您可能需要禁用 CSRF 保护。

CSRF protection and JSON

一个常见的问题是 "`do I need to protect JSON requests made by JavaScript?`"简短的答案是:这取决于情况。但是,你必须非常小心,因为有一些 CSRF 漏洞会影响 JSON 请求。例如,恶意用户可以创建一个 CSRF with JSON by using the following form

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 标头,那么它就会面临此漏洞。基于安装情况,Spring MVC 应用程序可以通过将 URL 后缀更新为以 .json 结尾来利用验证 Content-Type 中的漏洞,如下所示:

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 and Stateless Browser Applications

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

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

使用基本身份验证的应用程序也面临 CSRF 攻击的风险。此应用程序面临风险,因为浏览器自动在任何请求中包含用户名和密码,与在我们的前一个示例中发送 JSESSIONID Cookie 以相同的方式。

CSRF Considerations

实施针对 CSRF 攻击的保护时,有一些注意事项需要考虑。

Logging In

为了抵御 forging login requests,登录 HTTP 请求应受 CSRF 攻击保护。防止伪造登录请求是必要的,这样恶意用户就无法读取受害者的敏感信息。攻击的执行方式如下:

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

  2. 恶意用户随后欺骗受害者访问受感染网站并输入敏感信息。

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

确保 HTTP 登录请求受到 CSRF 攻击的保护可能会遇到的一个可能的麻烦是,用户可能遇到会话超时导致请求被拒绝的情况。对于原本不认为需要会话来登录的用户而言,会话超时是令人意外的。关于更多信息,请参阅 CSRF and Session Timeouts

Logging Out

为了防止伪造注销请求,注销 HTTP 请求应受 CSRF 攻击保护。防止伪造注销请求是必要的,这样恶意用户就无法读取受害者的敏感信息。有关攻击的详细信息,请参见 this blog post

确保 HTTP 注销请求受到 CSRF 攻击的保护可能会遇到的一个可能的麻烦是,用户可能遇到会话超时导致请求被拒绝的情况。对于原本不认为需要会话来注销的用户而言,会话超时是令人意外的。关于更多信息,请参见 CSRF and Session Timeouts

CSRF and Session Timeouts

大多数情况下,预期的 CSRF 令牌会存储在会话中。这意味着,一旦会话过期,服务器便找不到预期的 CSRF 令牌并拒绝 HTTP 请求。有很多选项(每个选项都带有权衡利弊的情况)可以解决超时问题:

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

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

  • 最后,可以将预期的 CSRF 令牌存储在 Cookie 中。这使得预期的 CSRF 令牌在会话中依然存在。可能会问为什么预期的 CSRF 令牌默认情况下不存储在 cookie 中。这是因为存在已知的漏洞,其中由其他域可以设置标头(例如,指定 cookie)。这与 Ruby on Rails no longer skips a CSRF checks when the header X-Requested-With is present相同。有关如何执行此漏洞利用的详细信息,请参阅 this webappsec.org thread。另一个缺点是通过移除状态(即超时),您将失去在令牌遭到破坏时强制使令牌失效的能力。

Multipart (file upload)

保护多部分请求(文件上传)免受 CSRF 攻击会导致 chicken or the egg问题。为了防止 CSRF 攻击发生,必须读取 HTTP 请求的正文以获取实际 CSRF 令牌。然而,读取正文意味着文件已上传,这意味着外部站点可以上传文件。

使用 multipart/form-data 进行 CSRF 保护有两种选择:

每个选项都有其权衡之处。

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

Place CSRF Token in the Body

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

Include CSRF Token in URL

如果允许未经授权的用户上传临时文件不可接受,另一种方法是将预期 CSRF 令牌作为查询参数包含在表单的action属性中。采用此方法的缺点是查询参数可能会泄露。总体而言,将敏感数据放在正文或标头中以确保其不被泄露被认为是最佳做法。您可以在 RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s中找到其他信息。

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 的过滤器之前仍然是最佳实践。