跳到主要内容

跨站请求伪造 (CSRF)

ChatGPT-4o-mini 中英对照 CSRF Cross Site Request Forgery (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>
html

对应的 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
none

现在假设你登录了银行网站,并且没有退出,访问了一个恶意网站。这个恶意网站包含一个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>
html

你喜欢赢取钱财,所以你点击了提交按钮。在这个过程中,你不小心将 $100 转账给了一个恶意用户。这发生是因为,尽管恶意网站无法看到你的 cookies,但与你银行相关的 cookies 仍然会随请求一起发送。

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

防御 CSRF 攻击

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

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

备注

这两种保护都要求 安全方法为只读

安全方法必须是只读

为了对 CSRF 进行 任一保护,应用程序必须确保 "安全" 的 HTTP 方法是只读的。这意味着使用 HTTP GETHEADOPTIONSTRACE 方法的请求不应改变应用程序的状态。

同步令牌模式

保护免受 CSRF 攻击的主要和最全面的方法是使用 同步令牌模式。该解决方案确保每个 HTTP 请求除了我们的会话 cookie 之外,还需要在 HTTP 请求中包含一个安全随机生成的值,称为 CSRF 令牌。

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

这项工作的关键在于,实际的 CSRF token 应该位于 HTTP 请求的一个部分,而该部分不会被浏览器自动包含。例如,要求将实际的 CSRF token 放在 HTTP 参数或 HTTP 头部,可以防止 CSRF 攻击。而将实际的 CSRF token 放在 cookie 中是行不通的,因为 cookies 会被浏览器自动包含在 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>
html

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

相应的 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
none

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

SameSite 属性

一种新兴的防护 CSRF 攻击 的方式是为 cookies 指定 SameSite 属性。服务器可以在设置 cookie 时指定 SameSite 属性,以指示当请求来自外部站点时,不应发送该 cookie。

备注

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

一个带有 SameSite 属性的 HTTP 响应头示例如下:

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

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 不会被发送,因此用户将无法进行身份验证。

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

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

何时使用 CSRF 保护

何时应该使用 CSRF 保护?我们的建议是,对于任何可能被浏览器正常用户处理的请求,都应该使用 CSRF 保护。如果您正在创建一个仅供非浏览器客户端使用的服务,您可能希望禁用 CSRF 保护。

CSRF 保护和 JSON

一个常见的问题是“我需要保护由 JavaScript 发出的 JSON 请求吗?”简短的回答是:这要看情况。然而,你必须非常小心,因为存在可能影响 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>
html

这将生成以下 JSON 结构

{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}
javascript

如果一个应用程序没有验证 Content-Type 头,它将面临这种攻击。根据设置,一个验证 Content-Type 的 Spring MVC 应用程序仍然可能通过将 URL 后缀更新为 .json 来受到攻击,如下所示:

<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>
html

CSRF 和无状态浏览器应用

如果我的应用程序是无状态的呢?这并不一定意味着你是安全的。实际上,如果用户在给定请求中不需要在网页浏览器中执行任何操作,他们仍然可能容易受到 CSRF 攻击。

例如,考虑一个使用自定义 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 token 存储在会话中。这意味着,一旦会话过期,服务器就无法找到预期的 CSRF token,从而拒绝 HTTP 请求。解决超时问题有多种方案(每种方案都有其利弊):

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

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

  • 最后,预期的 CSRF 令牌可以存储在一个 cookie 中。这使得预期的 CSRF 令牌可以超出会话的生命周期。

    有人可能会问,为什么预期的 CSRF 令牌默认不存储在 cookie 中。这是因为已知的漏洞可以让另一个域设置头部(例如,指定 cookies)。这也是 Ruby on Rails 不再在存在头部 X-Requested-With 时跳过 CSRF 检查的原因。有关如何执行该漏洞的详细信息,请参见 这个 webappsec.org 线程。另一个缺点是,通过移除状态(即超时),如果令牌被泄露,你将失去强制使令牌失效的能力。

Multipart (文件上传)

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

使用 multipart/form-data 时,有两种选项可以启用 CSRF 保护:

每个选项都有其权衡。

备注

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

将 CSRF 令牌放在请求体中

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

在 URL 中包含 CSRF 令牌

如果允许未经授权的用户上传临时文件不可接受,另一种选择是在表单的 action 属性中将期望的 CSRF 令牌作为查询参数包含进去。这种方法的缺点是查询参数可能会泄露。更一般来说,最佳实践是将敏感数据放在请求体或头部,以确保其不被泄露。您可以在 RFC 2616 Section 15.1.3 Encoding Sensitive Information in URI’s 中找到更多信息。

HiddenHttpMethodFilter

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

<form action="/process"
method="post">
<!-- ... -->
<input type="hidden"
name="_method"
value="delete"/>
</form>
html

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