跨站点请求伪造 (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 攻击的主要且最全面的方法是使用 同步令牌模式。此解决方案可确保除了我们的会话 Cookie 外,每个 HTTP 请求还需要在 HTTP 请求中提供一个称为 CSRF 令牌的安全随机生成值。
提交 HTTP 请求时,服务器必须查找预期的 CSRF 令牌,并将其与 HTTP 请求中的实际 CSRF 令牌进行比较。如果值不匹配,则应拒绝 HTTP 请求。
此方法的关键在于实际 CSRF 令牌应位于 HTTP 请求中不会自动包含的部分。例如,在 HTTP 参数或 HTTP 标头中要求实际 CSRF 令牌将防止 CSRF 攻击。在 Cookie 中要求实际 CSRF 令牌不起作用,因为 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 令牌进行比较时,转账将失败。
同站属性
Spring Security 不直接控制会话 cookie 的创建,因此它不支持同站属性。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,因此用户将无法通过身份验证。
我们可以通过实施 gh-7537 来提高 |
另一个显而易见的问题是,为了让 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 攻击的保护时,需要考虑一些特殊注意事项。
登录
为防止伪造登录请求,应针对 CSRF 攻击保护登录 HTTP 请求。防止伪造登录请求非常有必要,这样恶意用户才无法读取受害者的敏感信息。攻击执行如下
-
恶意用户使用恶意用户的凭据执行 CSRF 登录。受害者现在以恶意用户身份进行身份验证。
-
然后,恶意用户诱骗受害者访问受损网站并输入敏感信息。
-
该信息与恶意用户的帐户关联,因此恶意用户可以使用自己的凭据登录并查看受害者的敏感信息。
确保登录 HTTP 请求受到 CSRF 攻击保护的一个可能复杂之处在于,用户可能会遇到导致请求被拒绝的会话超时。对于那些不希望在登录时需要会话的用户来说,会话超时会让他们感到意外。有关更多信息,请参阅CSRF 和会话超时。
退出
为防止伪造注销请求,应针对 CSRF 攻击保护注销 HTTP 请求。防止伪造注销请求非常有必要,这样恶意用户才无法读取受害者的敏感信息。有关攻击的详细信息,请参阅此博客文章。
确保注销 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. 多部分解析器 部分和 |
将 CSRF 令牌放在主体中
第一个选项是将实际的 CSRF 令牌包含在请求的主体中。通过将 CSRF 令牌放在主体中,主体在执行授权之前被读取。这意味着任何人都可以将临时文件放在你的服务器上。但是,只有经过授权的用户才能提交应用程序处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。
将 CSRF 令牌包含在 URL 中
如果允许未经授权的用户上传临时文件不可接受,另一种方法是在表单的 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 的过滤器之前。