跳到主要内容
版本:7.0.2

跨站请求伪造 (CSRF)

DeepSeek V3 中英对照 CSRF Cross Site Request Forgery (CSRF)

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

备注

本文档的这一部分讨论 CSRF 保护的一般主题。有关基于 servlet 和 WebFlux 的应用程序的 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 GET、HEAD、OPTIONS 和 TRACE 方法的请求不应改变应用程序的状态。

同步令牌模式​

防范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的过滤器之前。