对于最新的稳定版本,请使用 Spring Security 6.5.3! |
跨站点请求伪造 (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
不应更改应用程序的状态。
同步器Tokens模式
防范 CSRF 攻击的主要和最全面的方法是使用同步器Tokens模式。 此解决方案旨在确保除了我们的会话 cookie 之外,每个 HTTP 请求还需要一个称为 CSRF Tokens的安全随机生成值 HTTP 请求中。
提交 HTTP 请求时,服务器必须查找预期的 CSRF Tokens,并将其与 HTTP 请求中的实际 CSRF Tokens进行比较。 如果值不匹配,则应拒绝 HTTP 请求。
这项工作的关键是,实际的 CSRF Tokens应该位于浏览器不会自动包含的 HTTP 请求的一部分中。 例如,要求在 HTTP 参数或 HTTP 标头中提供实际的 CSRF Tokens将防止 CSRF 攻击。 在 cookie 中要求实际的 CSRF Tokens不起作用,因为浏览器会自动将 cookie 包含在 HTTP 请求中。
我们可以放宽期望,只要求每个更新应用程序状态的 HTTP 请求提供实际的 CSRF Tokens。 为此,我们的应用程序必须确保安全的 HTTP 方法是幂等的。 这提高了可用性,因为我们希望允许使用来自外部网站的链接链接到我们的网站。 此外,我们不希望在 HTTP GET 中包含随机Tokens,因为这可能会导致Tokens泄露。
让我们看一下我们的示例在使用 Synchronizer Token Pattern 时将如何变化。
假设实际的 CSRF Tokens需要位于名为_csrf
.
我们申请的转账表格如下所示:
<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 Tokens,因为同源策略确保恶意站点无法读取响应。
相应的 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 Tokens与预期的 CSRF Tokens进行比较时,传输将失败。
SameSite 属性
防范 CSRF 攻击的一种新兴方法是在 cookie 上指定 SameSite 属性。
服务器可以指定SameSite
属性,以指示来自外部站点时不应发送 cookie。
Spring Security 不直接控制会话 cookie 的创建,因此它不提供对 SameSite 属性的支持。Spring Session 为 |
例如,HTTP 响应标头与SameSite
属性可能如下所示:
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
的有效值SameSite
属性是:
让我们看一下如何使用SameSite
属性。
银行应用程序可以通过指定SameSite
会话 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,其中包含其中的所有状态进行身份验证,而不是 JSESSIONID。 当进行 CSRF 攻击时,自定义 cookie 将与请求一起发送,其方式与上一个示例中发送 JSESSIONID cookie 的方式相同。 此应用程序将容易受到 CSRF 攻击。
使用基本身份验证的应用程序也容易受到 CSRF 攻击。 该应用程序很容易受到攻击,因为浏览器将以与上一个示例中发送 JSESSIONID cookie 相同的方式自动在任何请求中包含用户名和密码。
CSRF 注意事项
在实施针对 CSRF 攻击的保护时,需要考虑一些特殊注意事项。
登录
为了防止伪造登录请求,应保护登录 HTTP 请求免受 CSRF 攻击。 防止伪造登录请求是必要的,这样恶意用户就无法读取受害者的敏感信息。 攻击执行如下:
-
恶意用户使用恶意用户的凭据执行 CSRF 登录。 受害者现在被身份验证为恶意用户。
-
然后,恶意用户诱骗受害者访问受感染的网站并输入敏感信息
-
该信息与恶意用户的帐户相关联,因此恶意用户可以使用自己的凭据登录并查看 vicitim 的敏感信息
确保登录 HTTP 请求免受 CSRF 攻击的一个可能的复杂情况是,用户可能会遇到导致请求被拒绝的会话超时。 会话超时对于不需要会话才能登录的用户来说是令人惊讶的。 有关详细信息,请参阅 CSRF 和会话超时。
注销
为了防止伪造的注销请求,应保护注销 HTTP 请求免受 CSRF 攻击。 防止伪造的注销请求是必要的,这样恶意用户就无法读取受害者的敏感信息。 有关攻击的详细信息,请参阅此博客文章。
确保注销 HTTP 请求免受 CSRF 攻击的一个可能的复杂情况是,用户可能会遇到导致请求被拒绝的会话超时。 会话超时对于不需要会话才能注销的用户来说是令人惊讶的。 有关详细信息,请参阅 CSRF 和会话超时。
CSRF 和会话超时
通常情况下,预期的 CSRF Tokens存储在会话中。 这意味着一旦会话到期,服务器将找不到预期的 CSRF Tokens并拒绝 HTTP 请求。 解决超时问题有很多选择,每个选项都需要权衡。
-
缓解超时的最佳方法是使用 JavaScript 在表单提交时请求 CSRF Tokens。 然后,使用 CSRF Tokens更新表单并提交。
-
另一种选择是使用一些 JavaScript 让用户知道他们的会话即将过期。 用户可以单击一个按钮继续并刷新会话。
-
最后,预期的 CSRF Tokens可以存储在 cookie 中。 这允许预期的 CSRF Tokens比会话更有效。
有人可能会问,为什么默认情况下,预期的 CSRF Tokens不存储在 cookie 中。 这是因为存在已知的漏洞,其中标头(例如,用于指定 cookie)可以由另一个域设置。 这与 Ruby on Rails 在标头 X-Requested-With 存在时不再跳过 CSRF 检查的原因相同。 有关如何执行该漏洞的详细信息,请参阅此 webappsec.org 线程。 另一个缺点是,通过删除状态(即超时),您将失去在Tokens被泄露时强制使Tokens失效的能力。
分段(文件上传)
保护分段请求(文件上传)免受 CSRF 攻击会导致先有鸡还是先有蛋的问题。 为了防止发生 CSRF 攻击,必须读取 HTTP 请求的正文以获取实际的 CSRF Tokens。 但是,读取正文意味着将上传文件,这意味着外部站点可以上传文件。
将 CSRF 保护与多部分/表单数据一起使用有两个选项。 每个选项都有其权衡。
在将 Spring Security 的 CSRF 保护与多部分文件上传集成之前,请确保您可以先在没有 CSRF 保护的情况下上传。 有关在 Spring 中使用多部分表单的更多信息,可以在 1.1.11 中找到。Spring 参考的 Multipart Resolver 部分和 MultipartFilter javadoc。 |
将 CSRF Tokens放入正文中
第一个选项是在请求正文中包含实际的 CSRF Tokens。 通过将 CSRF Tokens放在正文中,将在执行授权之前读取正文。 这意味着任何人都可以在您的服务器上放置临时文件。 但是,只有授权用户才能提交由您的应用程序处理的文件。 通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响应该可以忽略不计。
在 URL 中包含 CSRF Tokens
如果不允许未经授权的用户上传临时文件,则另一种方法是将预期的 CSRF Tokens作为查询参数包含在表单的 action 属性中。 这种方法的缺点是查询参数可能会泄漏。 更一般地说,将敏感数据放置在正文或标头中以确保其不会泄露被认为是最佳实践。 其他信息可以在 RFC 2616 第 15.1.3 节 对 URI 中的敏感信息进行编码中找到。
HiddenHttp方法过滤器
在某些应用程序中,可以使用表单参数来覆盖 HTTP 方法。
例如,下面的表单可用于将 HTTP 方法视为delete
而不是post
.
<form action="/process"
method="post">
<!-- ... -->
<input type="hidden"
name="_method"
value="delete"/>
</form>
重写 HTTP 方法发生在过滤器中。
该过滤器必须放在 Spring Security 的支持之前。
请注意,覆盖仅发生在post
,所以这实际上不太可能引起任何真正的问题。
但是,确保将其放置在 Spring Security 的过滤器之前仍然是最佳实践。