跨站请求伪造 (CSRF)
Spring 为防范 跨站请求伪造 (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 美元。发生这种情况是因为,尽管恶意网站无法看到你的 cookie,但与你的银行相关的 cookie 仍然随请求一起发送。
更糟糕的是,整个过程可以通过使用 JavaScript 自动化。这意味着你甚至不需要点击按钮。此外,当访问一个受 XSS 攻击 影响的诚信网站时,这种情况也可能发生。那么我们如何保护用户免受此类攻击呢?
防范 CSRF 攻击
CSRF 攻击之所以可能,是因为来自受害者网站的 HTTP 请求和来自攻击者网站的请求完全相同。这意味着无法拒绝来自恶意网站的请求,只允许来自银行网站的请求。为了防范 CSRF 攻击,我们需要确保请求中包含恶意网站无法提供的内容,以便我们可以区分这两个请求。
Spring 提供了两种机制来防范 CSRF 攻击
-
在会话 cookie 上指定 SameSite 属性
|
两种保护都要求 安全方法是只读的。 |
安全方法必须是只读的
为了使 任一保护 免受 CSRF 攻击生效,应用程序必须确保 “安全” HTTP 方法是只读的。这意味着使用 HTTP GET、HEAD、OPTIONS 和 TRACE 方法的请求不应更改应用程序的状态。
同步器令牌模式
防范 CSRF 攻击的主要且最全面的方法是使用 同步器令牌模式。此解决方案旨在确保每个 HTTP 请求除了我们的会话 cookie 之外,还需要在 HTTP 请求中包含一个安全的随机生成值,称为 CSRF 令牌。
当提交 HTTP 请求时,服务器必须查找预期的 CSRF 令牌并将其与 HTTP 请求中的实际 CSRF 令牌进行比较。如果值不匹配,则应拒绝 HTTP 请求。
此方法奏效的关键在于,实际的 CSRF 令牌应位于 HTTP 请求中不会被浏览器自动包含的部分。例如,要求实际 CSRF 令牌位于 HTTP 参数或 HTTP 头部中将有助于防范 CSRF 攻击。要求实际 CSRF 令牌位于 cookie 中不起作用,因为 cookie 会被浏览器自动包含在 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>
该表单现在包含一个带有 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 属性的 HTTP 响应头示例可能看起来像
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
SameSite 属性的有效值为
考虑一下 我们的示例 如何使用 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>
这将生成以下 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 攻击的保护。保护登录请求免受伪造是必要的,这样恶意用户就无法读取受害者的敏感信息。攻击按如下方式执行
-
恶意用户使用恶意用户的凭据执行 CSRF 登录。受害者现在已以恶意用户的身份进行身份验证。
-
恶意用户然后诱骗受害者访问受损网站并输入敏感信息。
-
这些信息与恶意用户的帐户相关联,因此恶意用户可以使用自己的凭据登录并查看受害者的敏感信息。
确保登录 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 令牌。但是,读取主体意味着文件已上传,这意味着外部站点可以上传文件。
使用 CSRF 保护处理 multipart/form-data 有两种选择
每个选项都有其权衡。
|
在将 Spring Security 的 CSRF 保护与多部分文件上传集成之前,你应该首先确保可以在没有 CSRF 保护的情况下上传。有关使用 Spring 的多部分表单的更多信息,请参阅 Spring 参考文档的 1.1.11. 多部分解析器 部分和 |
将 CSRF 令牌放在主体中
第一个选项是将实际的 CSRF 令牌包含在请求的主体中。通过将 CSRF 令牌放在主体中,在执行授权之前会读取主体。这意味着任何人都可以将临时文件放在你的服务器上。但是,只有经过授权的用户才能提交由你的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。
在 URL 中包含 CSRF 令牌
如果允许未经授权的用户上传临时文件是不可接受的,另一种方法是将预期的 CSRF 令牌作为查询参数包含在表单的 action 属性中。这种方法的缺点是查询参数可能会泄露。更普遍地,将敏感数据放在主体或头部中以确保其不被泄露被认为是最佳实践。你可以在 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 的过滤器之前。