Cross Site Request Forgery (CSRF)
本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
Spring 提供了对 跨站请求伪造(CSRF) 攻击的全面支持。在下面的章节中,我们将探讨。
什么是CSRF攻击?
了解CSRF攻击的最好方法是看一个具体的例子。
假设你的银行网站提供了一个表单(form),允许将当前登录的用户的钱转到另一个银行账户。例如,这个转账表格可能看起来像下面这样。
<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)。
<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>
你喜欢赢钱(Win Money),所以你点击了提交按钮。在这个过程中,你无意中把100美元转给了一个恶意的用户。发生这种情况的原因是,虽然“邪恶网站”看不到你的cookie,但与你的银行相关的cookie仍然与请求一起被发送。
更糟糕的是,这整个过程本来可以通过使用JavaScript自动完成。这意味着你甚至不需要点击这个按钮。此外,在访问一个遭受 XSS攻击 的“诚实网站”时,它也可能很容易发生。那么,我们如何保护我们的用户免受此类攻击?
防范CSRF攻击
CSRF攻击之所以可能,是因为来自受害者网站的HTTP请求和来自攻击者网站的请求是完全相同的。这意味着没有办法拒绝来自“邪恶网站”的请求而只允许来自银行网站的请求。为了防止CSRF攻击,我们需要确保请求中存在“邪恶网站”无法提供的东西,这样我们就可以区分这两个请求。
Spring提供了两种机制来防止CSRF攻击。
-
在你的 session cookie 上指定 SameSite 属性
这两种保护措施都要求 Safe Method 必须是幂等的。 |
Safe Method 必须是幂等的
为了使对CSRF的保护发挥作用,应用程序必须确保 安全的HTTP请求方法必须是幂等的 的。这意味着使用 HTTP GET
、HEAD
、OPTIONS
和 TRACE
方法的请求不应该改变应用程序的状态。
同步令牌(Synchronizer Token)模式
防止CSRF攻击的最主要和最全面的方法是使用 Synchronizer Token 模式。这个解决方案是确保每个HTTP请求除了需要我们的会话cookie外,还需要在HTTP请求中出现一个被称为CSRF令牌的安全随机生成值。
当一个HTTP请求被提交时,服务器必须查找预期的CSRF令牌,并将其与HTTP请求中的实际CSRF令牌进行比较。如果数值不匹配,HTTP请求应被拒绝。
这个工作的关键是,实际的CSRF令牌应该在HTTP请求的某个部分,而不是由浏览器自动包含。例如,在HTTP参数或HTTP header 中要求实际的CSRF令牌可以防止CSRF攻击。在cookie中要求实际的CSRF令牌不起作用,因为cookie会被浏览器自动包含在HTTP请求中。
我们可以放宽预期,只要求每个更新应用程序状态的HTTP请求提供实际的CSRF令牌。要做到这一点,我们的应用程序必须确保 Safe Method 必须是幂等的。这提高了可用性,因为我们希望允许从外部网站链接到我们的网站。此外,我们不希望在HTTP GET中包含随机令牌(Token),因为这可能导致令牌被泄露。
考虑一下当我们使用 Synchronizer Token 模式时,我们的例子会有什么变化。假设实际的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>
表单现在包含一个隐藏的 input,其中有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上指定 the SameSite 属性。服务器可以在设置cookie时指定 SameSite
属性,以表明来自外部网站的cookie不应该被发送。
Spring Security 不直接控制session cookie的创建,所以它不提供对 |
一个例子,带有 SameSite
属性的HTTP响应头可能看起来像下面这样。
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
SameSite
属性的有效值如下。
-
Strict
: 当指定时,任何来自 同一站点 的请求都包括该cookie。否则,cookie不包括在HTTP请求中。 -
Lax
: 当指定时,当来自 同一站点 或请求来自顶级导航且 Safe Method 必须是幂等的 时,将发送cookie。否则,cookie不包括在HTTP请求中。
关于 SameSite 属性的详细内容可以参考: Cookie 的 SameSite 属性
|
考虑一下我们的例子如何使用 SameSite
属性进行保护。银行应用程序可以通过在会话cookie上指定 SameSite
属性来防止CSRF。
在我们的 Session cookie 上设置了 SameSite
属性后,浏览器会在来自银行网站的请求中继续发送 JESSIONID
cookie。然而,在来自“邪恶网站”的传输请求中,浏览器不再发送 JESSIONID
cookie。由于session 不再出现在来自“邪恶网站”的传输请求中,应用程序被保护免受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请求吗?" 简短的回答是:看情况。然而,你必须非常小心,因为有一些CSRF漏洞可以影响JSON请求。例如,一个恶意的用户可以 通过使用以下形式用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和无状态浏览器应用
如果我的应用程序是无状态的呢?这并不一定意味着你得到了保护。事实上,如果用户不需要在网络浏览器中对某一请求进行任何操作,他们很可能仍然容易受到CSRF攻击。
例如,考虑一个使用自定义cookie的应用程序,该cookie中包含了所有用于验证的状态(而不是JSESSIONID)。当CSRF攻击发生时,自定义cookie与请求一起被发送,其方式与我们前面的例子中的JSESSIONID cookie被发送的方式相同。这个应用程序很容易受到CSRF攻击。
使用基本认证的(basic authentication)应用程序也容易受到CSRF攻击。由于浏览器在任何请求中都会自动包含用户名和密码,与我们之前的例子中发送JSESSIONID cookie的方式相同,所以该应用程序容易受到攻击。
CSRF的考虑因素
在实施对CSRF攻击的保护时,有一些特殊的考虑因素需要考虑。
登录
为了防止 伪造登录请求,应该保护登录的HTTP请求免受CSRF攻击。防止伪造登录请求是必要的,这样恶意的用户就不能读取受害者的敏感信息。攻击的方式如下。
-
恶意用户用“恶意用户的凭证”进行CSRF登录。受害者现在被认证为恶意用户。
-
恶意用户然后欺骗受害者访问被攻击的网站并输入敏感信息。
-
这些信息与恶意用户的账户相关联,因此恶意用户可以用他们自己的凭证登录并查看受害者的敏感信息。
确保登录的HTTP请求免受CSRF攻击的一个可能的复杂情况是,用户可能会遇到会话超时的情况,导致请求被拒绝。会话超时对于那些不期望需要会话来登录的用户来说是令人惊讶的。欲了解更多信息,请参考CSRF 和会话(Session)超时。
退出登录
为了防止伪造的注销请求,注销的HTTP请求应该被保护起来,以防止CSRF攻击。防止伪造注销请求是必要的,这样恶意的用户就不能读取受害者的敏感信息。关于攻击的细节,请看 这篇博文。
要确保注销HTTP请求受到CSRF攻击的保护,一个可能的复杂情况是,用户可能会遇到会话超时的情况,导致请求被拒绝。会话超时对于那些不期望有会话来注销的用户来说是令人惊讶的。欲了解更多信息,请参阅 CSRF 和会话(Session)超时。
CSRF 和会话(Session)超时
更多时候,预期的CSRF令牌被存储在会话中。这意味着,一旦会话过期,服务器就找不到预期的CSRF令牌而拒绝HTTP请求。有许多选项(每个选项都有交换条件)来解决超时问题。
-
缓解超时的最好方法是使用JavaScript在表单提交时请求一个CSRF令牌。然后用CSRF令牌更新表单并提交。
-
另一个选择是有一些JavaScript,让用户知道他们的会话即将到期。用户可以点击一个按钮来继续并刷新会话。
-
最后,预期的CSRF令牌可以存储在一个cookie中。这可以让预期的CSRF令牌在会话中失效。
有人可能会问,为什么预期的CSRF令牌默认不存储在cookie中。这是因为有一些已知的漏洞,在这些漏洞中,header 信息(例如,用于指定cookie)可以由另一个域来设置。这与Ruby on Rails 在
X-Requested-With
header出现时不再跳过CSRF检查的原因相同。关于如何执行该漏洞的细节,请参见 webappsec.org 的这个文章。另一个缺点是,通过移除状态(即超时),你就失去了在令牌被泄露时强行使其失效的能力。
Multipart (文件上传)
保护 Multipart 请求(文件上传)免受CSRF攻击会导致一个 鸡或蛋 的问题。为了防止CSRF攻击的发生,必须读取HTTP请求的主体以获得实际的CSRF令牌。然而,读取正文意味着文件被上传,这意味着一个外部网站可以上传文件。
有两种方法可以使用CSRF保护 multipart/form-data
。
每种选择都有其利弊得失。
在你将 Spring Security 的 CSRF 保护与 multipart 文件上传整合之前,你应该首先确保你可以在没有CSRF保护的情况下进行上传。关于在 Spring 中使用 multipart form 的更多信息,请参见Spring参考资料的 1.1.11. Multipart Resolver 部分和 |
在请求体中放置CSRF令牌
第一个选项是将实际的CSRF令牌包含在请求的body中。通过将CSRF令牌放在请求体中,在进行授权之前就会读取请求体。这意味着,任何人都可以在你的服务器上放置临时文件。然而,只有经过授权的用户可以提交一个由你的应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件的上传对大多数服务器的影响应该是可以忽略不计的。
在URL中放置CSRF令牌
如果让未经授权的用户上传临时文件是不可接受的,另一种方法是将预期的CSRF令牌作为查询参数包含在表单的action属性中。这种方法的缺点是查询参数可能被泄露。更普遍的做法是,将敏感数据放在 body 或 header 中,以确保其不被泄露,这是最佳做法。你可以在 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 的过滤器之前。