跳到主要内容
版本:7.0.2

跨站请求伪造 (CSRF)

DeepSeek V3 中英对照 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>

对应的 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美元。这是因为,虽然恶意网站无法看到你的cookies,但与你银行账户关联的cookies仍然会随着请求一同发送。

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

防范 CSRF 攻击

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

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

备注

两种防护措施都要求安全方法必须是只读的

安全方法必须是只读的

为了防范CSRF攻击能够生效,应用程序必须确保"安全"的HTTP方法是只读的。这意味着使用 HTTP GETHEADOPTIONSTRACE 方法的请求不应改变应用程序的状态。

同步令牌模式

防范CSRF攻击最主要且最全面的方法是采用同步令牌模式。该解决方案确保每个 HTTP 请求除了需要会话 cookie 外,还必须在请求中包含一个称为 CSRF 令牌的安全随机生成值。

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

实现此机制的关键在于,实际的CSRF令牌必须位于HTTP请求中不会被浏览器自动包含的部分。例如,要求在实际的HTTP参数或HTTP头部中携带CSRF令牌可有效防御CSRF攻击。若将实际CSRF令牌置于cookie中则无法生效,因为浏览器会自动在HTTP请求中包含cookie。

我们可以放宽要求,仅对每个会更新应用程序状态的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 响应头示例如下:

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 不会被发送,用户将无法保持认证状态。

另一个显而易见的考量是,为了使 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>

这将生成以下JSON结构

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

如果应用程序未验证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>

无状态浏览器应用中的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令牌。然而,读取正文意味着文件已被上传,这也就意味着外部站点可以上传文件。

在使用 multipart/form-data 时,有两种启用 CSRF 防护的选项:

每种选择都有其权衡之处。

备注

在将 Spring Security 的 CSRF 防护与多部分文件上传集成之前,你首先应确保在不启用 CSRF 防护的情况下能够正常上传。关于在 Spring 中使用多部分表单的更多信息,请参阅 Spring 参考文档中的 1.1.11. 多部分解析器 章节以及 MultipartFilter Javadoc

将 CSRF 令牌置于请求体中

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

在URL中包含CSRF令牌

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

HiddenHttpMethodFilter

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

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

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