防止跨站点请求伪造攻击 (CSRF/XSRF)的最常见方法是使用反 CSRF 令牌,该令牌只是一个唯一值集,然后由 Web 应用程序需要。CSRF 是一种客户端攻击,可用于将用户重定向到恶意网站、窃取敏感信息或在用户会话中执行其他操作。幸运的是,使用 CSRF 令牌来保护用户免受 CSRF 攻击及其后果相对容易。
CSRF原理
那我们具体看看攻击细节
看图说话,大致过程
- 用户访问浏览正常网站
- 正常网站服务器响应并且返回标识该用户身份的cookie
- 用户未注销正常网站的情况下,访问恶意网站
- 恶意网站里访问正常网站并且带着标识用户的cookie
- 正常网站服务器接受来自恶意网站的请求
再次访问正常网站时,浏览器会自动带上标识该用户身份的cookie发送请求,所以正常网站服务器会接受来自恶意网站的请求,从而完成攻击。
当我访问登录一个正常网站,成功访问后服务器会产生一个标识用户身份的cookie给用户的浏览器保存,在标识cookie还存在时访问恶意网站,在该网站里攻击者会让你不知不觉的访问之前的正常网站并且执行一些操作,由于标识用户身份的cookie还存在,所以用户浏览器认为是用户的本意操作而执行该请求,从而攻击成功。
这些欺骗的访问方式有很多,例如“点击小广告、找回密码”等等诱导用户去点击操作。
CSRF 攻击类型
如果 Web 应用程序无法区分非法请求和有效请求,那么它很容易受到跨站点请求伪造的攻击。有当认证用户与web应用程序处于活动会话时,攻击才会成功。
登录 CSRF 攻击
使用一种称为Login CSRF攻击的特殊形式的CSRF攻击,攻击者可以访问受害者用户的机密和敏感数据。这种攻击迫使不知情的用户登录到黑客控制的帐户。然后,受害者被骗向账户中添加个人身份识别和敏感信息,如电子邮件地址和信用卡信息。
存储 CSRF 攻击
CSRF攻击包也可以存储在易受攻击的网站上。对手可以通过执行高级XSS攻击或在接受HTML的字段中存储IFRAME或IMG标记来实现存储CSRF攻击。在站点上存储攻击会放大攻击的严重性和可能性,因为受害者用户现在肯定会查看CSRF加载的页面,并且也会自然地对该页面进行身份验证
防止 CSRF 攻击的一些方法包括
- 使用同步器令牌模式
- 使用双重提交 cookie
- 使用 HTTP 标准标头来验证请求的来源
- 基于 UI 的验证,例如基于 CAPTCHA 的授权和 MFA
- 使用 SameSite Cookie 进行请求来源管理
- CSRF 令牌的使用
以下是一些防御 CSRF 攻击的方法:
-
使用POST请求
由于伪造GET请求的难度相对较小(只需打开URL或请求资源),因此对于涉及关键业务操作的接口,尽量使用POST请求。这是因为POST请求通常需要更多的条件和数据,使攻击者更难以伪造有效的请求。
-
增加验证码
验证码被认为是对抗 CSRF 攻击最简洁而有效的防御方法。
从上述示例中可以看出,CSRF 攻击往往是在用户不知情的情况下构造了网络请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求。因为通常情况下,验证码能够很好地遏制 CSRF 攻击。
但验证码并不是万能的,因为出于用户考虑,不能给网站所有的操作都加上验证码。因此,验证码只能作为防御 CSRF 的一种辅助手段,而不能作为最主要的解决方案。
通过增加验证码等验证手段,只有通过验证的请求才被视为合法请求。此方法确保了请求的真实性,以防止被恶意伪造的请求成功执行。
-
设置SameSite属性
CSRF攻击的特点之一是,伪造请求的域名通常不是受攻击网站。通过设置Cookies的SameSite属性为Strict,仅允许同源网站发送带有Cookies的请求。这样可以阻止跨域攻击者在用户的浏览器中执行伪造的请求。需要注意的是,这种方法可能受到一些不兼容的浏览器版本的限制。
-
验证Referer
由于伪造请求通常不会包含正确的Referer信息,可以根据请求头中的Referer字段识别请求的来源是否可信。通过验证Referer,可以防止不受信任的请求进行伪造。然而,攻击者可能通过设置请求不携带Referer来绕过这种防御措施,因此这只能作为辅助的防御方法。
根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的”源”。
比如,如果用户要删除自己的帖子,那么先要登录 www.c.com,然后找到对应的页面,发起删除帖子的请求。此时,Referer 的值是 http://www.c.com;当请求是从 www.a.com 发起时,Referer 的值是 http://www.a.com 了。因此,要防御 CSRF 攻击,只需要对于每一个删帖请求验证其 Referer 值,如果是以 www.c.com 开头的域名,则说明该请求是来自网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是 CSRF 攻击,可以拒绝该请求。
针对上文的例子,可以在服务端增加如下代码:
1
2
3
4
if
(req.headers.referer !==
'http://www.c.com:8002/'
) {
res.write(
'csrf 攻击'
);
return
;
}
Referer Check 不仅能防范 CSRF 攻击,另一个应用场景是 “防止图片盗链”。Referer 这个方法缺点是攻击者有可能篡改该Referer字段内容,从而欺骗服务器。
-
验证CSRF令牌
这是一种广泛应用的有效防御方法。服务器在每个会话中生成一个随机的CSRF令牌,并将其保存在会话中和客户端的 .Cookies 或 .表单字段 中。在发送请求时,客户端要么将令牌作为HTTP请求的头部,要么作为参数发送给服务器。服务器收到请求后,验证请求中的令牌是否与会话中的令牌匹配。只有当令牌匹配时,请求才被视为合法请求。这种方法通常适用于非分离式和分离式的项目。对于非分离式项目,令牌可以直接写入模板中的隐藏表单字段中,以实现发送请求时不需要额外的操作。而对于分离式项目,令牌可以在登录时写入Cookies中,在发送请求时,JavaScript从Cookies中读取令牌,并设置为HTTP请求的头部。
-
更换登录态方案
由于CSRF攻击本质上是利用受害者在浏览器中已经验证的会话,如果替换会话验证机制,例如使用JSON Web Token(JWT),其令牌信息通常存储在HTTP头部中,可以有效防止CSRF攻击。
以下是基于 JWT 在防御 CSRF 攻击方面的一些关键优势:
(1)CSRF Token 不再必需:传统的基于 Session 的方案通常需要使用 CSRF Token 来在表单或请求中进行验证,以阻止 CSRF 攻击。而在基于 JWT 的方案中,JWT 本身包含了验证信息,不需要额外的 CSRF Token,从而简化了实现和管理的复杂性。
(2)阻止跨域请求:基于 JWT 的方案通常会将 JWT 存储在请求的 Header 中,例如 Authorization Header,而跨域请求通常无法携带自定义 Header。这样,攻击者无法在跨域请求中发送有效的 JWT,从而有效地阻止 CSRF 攻击。
(3)验证令牌完整性:在传统的基于 Session 的方案中,服务器需要从会话存储中获取与请求关联的 Session ID,并进行验证。而在基于 JWT 的方案中,服务器只需要验证 JWT 的签名和有效期,确保 JWT 的完整性和合法性。
需要注意的是,尽管基于 JWT 的方案提供了一些优势来防御 CSRF 攻击,本文只是基于 防御 CSRF 攻击来阐述了 JWT 的优势,但最终选择合适的登录态方案是根据应用需求、安全性要求和技术架构来决定的。无论选择基于 Session 还是基于 JWT 的方案,都需要遵循相关的安全最佳实践,以确保登录态的安全性和应用的整体安全性。
反 CSRF 令牌基础知识
反 CSRF 令牌(也称为同步器令牌模式或简称 CSRF 令牌)背后的想法是为用户的浏览器提供一条信息(令牌),然后浏览器必须发回该信息。令牌必须是唯一的并且第三方无法猜测,并且应用程序必须在令牌经过验证后才处理 HTTP 请求。这确保只有原始用户才能在经过身份验证的会话中发送请求。
对于没有 CSRF 保护的基本示例,假设您在www.example.com上运行一个 Web 应用程序。要在应用程序中的个人资料上发布消息,用户需要填写 HTML 表单并单击“提交”按钮:
<form action="/action.php" method="post">
Subject: <input type="text" name="subject"/><br/>
Content: <input type="text" name="content"/><br/>
<input type="submit" value="Submit"/>
</form>
提交操作会导致 Web 浏览器向服务器发送 POST 请求,并将用户输入的任何数据作为参数发送:
POST /post.php HTTP/1.1
Host: example.com
subject=I am feeling well&content=I just ate a cookie and it was delicious
如果用户已登录并且攻击者知道请求语法,则可能会使用 CSRF 攻击在该用户的个人资料上发布广告:
<form action="/action.php" method="post">
Subject: <input type="text" name="subject" value="Buy my product!"/>
Content: <input type="text" name="content" value="To buy my product, visit this site: example.biz."/>
<input type="submit" value="Submit"/>
</form>
<script>
document.forms[0].submit();
</script>
结果,Web 浏览器发送以下 POST 请求:
POST /post.php HTTP/1.1
Host: example.com
subject=Buy my product!&content=To buy my product, visit this site: example.biz.
在未受保护的页面上,如果服务器将伪造的请求视为来自经过身份验证的用户,则可能会实现 CSRF。
但现在假设您的站点使用简单的基于令牌的 CSRF 缓解措施,并且您的 Web 服务器在登录后立即发送到浏览器的会话 cookie 中设置令牌。然后,所有表单提交都包含一个包含令牌的隐藏字段。假设正确的令牌验证,这完全消除了 CSRF 漏洞:
<form>
Subject: <input type="text" name="subject"/><br/>
Content: <input type="text" name="content"/><br/>
<input type="submit" value="Submit"/>
<input type="hidden" name="token" value="R6B7hoBQd0wfG5Y6qOXHPNm4b9WKsTq6Vy6Jssxb"/>
</form>
然后,服务器应该只接受来自同一用户的包含此确切令牌值的 POST 请求,例如:
POST /post.php HTTP/1.1
Host: example.com
subject=I am feeling well&content=I just ate a cookie and it was delicious.&token=R6B7hoBQd0wfG5Y6qOXHPNm4b9WKsTq6Vy6Jssxb
有了这种保护,尝试使用恶意站点执行 CSRF 的攻击者在不知道有效用户 cookie 中设置的当前令牌的情况下无法伪造 HTTP 请求。由于您的服务器拒绝没有此令牌的所有请求,因此任何攻击尝试都将失败。
如何生成和验证 CSRF 令牌
无论您使用什么具体方法来生成和验证反 CSRF 令牌,请确保遵循以下首要安全规则,以防止攻击者在其恶意请求中伪造令牌:
- 使用具有足够熵的信誉良好且不可预测的随机数生成器。
- 令牌在短时间内过期以防止重复使用。
- 检查接收到的令牌是否与设置的令牌相同时,请使用安全比较方法(例如比较加密哈希)。
- 切勿在 HTTP GET 请求中发送 CSRF 令牌,以确保它们永远不会显示在 URL 中,并且不会
Referer
与其他引用者信息一起泄漏在标头中。
例如,在 PHP 中,您可以生成基本令牌,如下所示:
$_SESSION['token'] = bin2hex(random_bytes(24));
当您收到传入令牌时,您可以通过比较哈希值来安全地验证它:
if (hash_equals($_SESSION['token'], $_POST['token'])) {
// Action if token is valid
} else {
// Action if token is invalid
}
每个表单的 CSRF 保护
使用上述基本的反 CSRF 令牌,您可以在登录时在用户会话 cookie 中设置令牌,然后为每个表单验证相同的令牌。在大多数情况下,这种保护就足够了,但某些站点可能需要更安全的方法。为了平衡安全性和可用性,您可以为您使用的每个表单生成单独的令牌。
为此,请生成一个令牌,但不要将其直接公开给用户的浏览器。相反,将令牌与表单的文件名组合进行哈希处理,例如:
hash_hmac('sha256', 'post.php', $_SESSION['internal_token'])
要进行验证,请比较以这种方式生成的两个哈希值。如果令牌有效并且使用相同的形式,则哈希值将匹配。
针对每个请求的 CSRF 保护
当需要非常高级别的保护时(也许在银行应用程序中),您可以为每个请求使用单独的令牌,只需在验证后使每个令牌失效即可。
这种方法有几个可用性缺陷,在实现每个请求令牌之前应该仔细考虑这些缺陷。
- 1、最值得注意的是,它使得无法在多个选项卡中使用该应用程序。
- 2、您也无法使用浏览器的后退按钮,只能使用应用程序的内部导航功能。
- 3、由于每个请求都需要一个新的随机令牌,因此每个请求的保护需要考虑服务器性能并使用资源密集程度较低的随机数生成器。
使用非持久性 CSRF 令牌
如果您的网页或应用程序非常繁忙并且服务器存储空间有限,您可能希望完全避免在服务器端保留令牌。在这些特定情况下,您可以以加密方式生成和处理令牌,而无需将它们存储在服务器会话中:
- 使用对称加密,其密钥只有服务器知道,从不与客户端共享。
- 通过组合当前时间戳、用户名和表单名称(如果需要)生成令牌,然后使用服务器密钥加密整个字符串。
- 当您从 Web 浏览器收到令牌时,请使用同一密钥对其进行解密。
- 检查解密令牌的时间戳(以消除旧令牌),并将解密的用户和表单名称与预期的当前值进行比较。
虽然这种方法不需要服务器端存储,但它可能会产生一些性能开销,因为加密函数比简单的随机数生成更消耗资源。
实现非持久性令牌的另一个选项是双重提交 cookie。对于此技术,服务器甚至在用户进行身份验证之前就在 cookie 中设置随机值。然后,服务器期望该值随每个请求一起发送(例如,使用隐藏的表单字段)。
针对异步 (Ajax) 请求的 CSRF 保护
反 CSRF 令牌还应该用于 Ajax 请求。为了安全地实现它们,首先确保您的 Web 服务器不允许跨域异步请求(通过检查跨源资源共享标头)。
在为 Ajax 请求实现 CSRF 保护时,您可以像往常一样将令牌包含在隐藏文本字段中,或者直接将其放入 JavaScript 中。然后,您可以通过每个异步请求发送令牌,并在服务器端验证其存在和值。
登录表单的反 CSRF 令牌
人们普遍认为,仅当用户登录时才需要反 CSRF 令牌,因此登录表单不需要 CSRF 保护。虽然确实无法在用户登录之前模拟用户,但登录表单缺乏 CSRF 保护可能会导致攻击在诱骗用户以攻击者身份登录后暴露敏感信息。攻击可以如下执行:
- 攻击者在您的 Web 应用程序中创建一个帐户。
- 攻击者诱骗受害者使用攻击者的凭据登录您的应用程序,如果登录表单上没有 CSRF 保护,则可能通过一些社会工程实现这一点。
- 受害者正常使用您的应用程序,可能不知道他们以其他人的身份登录。
- 然后,攻击者可能会使用历史记录功能跟踪受害者,或以其他方式从受害者与您的应用程序的交互中获利。
为了最大限度地降低这些攻击和相关攻击的风险,最佳做法是在所有登录页面上包含反 CSRF 令牌。
与登录表单一样,您可能还会看到建议不要对 REST API 端点使用反 CSRF 令牌的在线资源,声称它们是不必要的。虽然在许多情况下这在技术上可能是正确的,但通常很难预测访问 API 的所有方式(以及将来可能修改的方式),因此为 REST API 提供 CSRF 保护可能会被视为额外的安全层。
超越代币:其他 CSRF 预防方法
为了深入防御 CSRF,您可以将 CSRF 令牌与其他技术结合起来。例如,要验证 Ajax 请求,您可以添加然后检查任意自定义标头。这是有效的,因为同源策略规定只能使用来自同源的 JavaScript 来添加请求标头,但请注意,您不应依赖此行为作为唯一的防线。有关此方法和其他 CSRF 预防方法的详细讨论,请参阅OWASP 跨站点请求伪造预防备忘单。
反 CSRF 令牌是防御 CSRF 攻击的最安全方法之一,但应用程序中的其他漏洞可能允许攻击者绕过 CSRF 保护。例如,如果您的 Web 应用程序存在跨站点脚本漏洞 (XSS),则攻击者可能会使用 XSS 执行脚本,该脚本会使用当前(有效)CSRF 令牌以静默方式获取新版本的表单。为了防止这种情况发生并维护可靠的 Web 应用程序安全性,请确保定期扫描 Web 应用程序是否存在所有类型的漏洞,而不仅仅是 CSRF。
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
。
-
在请求体中放置CSRF令牌
-
在URL中放置CSRF令牌
每种选择都有其利弊得失。
在你将 Spring Security 的 CSRF 保护与 multipart 文件上传整合之前,你应该首先确保你可以在没有CSRF保护的情况下进行上传。关于在 Spring 中使用 multipart form 的更多信息,请参见Spring参考资料的 1.1.11. Multipart Resolver 部分和 MultipartFilter Javadoc。 |
在请求体中放置CSRF令牌
第一个选项是将实际的CSRF令牌包含在请求的body中。通过将CSRF令牌放在请求体中,在进行授权之前就会读取请求体。这意味着,任何人都可以在你的服务器上放置临时文件。然而,只有经过授权的用户可以提交一个由你的应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件的上传对大多数服务器的影响应该是可以忽略不计的。
在URL中放置CSRF令牌
如果让未经授权的用户上传临时文件是不可接受的,另一种方法是将预期的CSRF令牌作为查询参数包含在表单的action属性中。这种方法的缺点是查询参数可能被泄露。更普遍的做法是,将敏感数据放在 body 或 header 中,以确保其不被泄露,这是最佳做法。你可以在 RFC 2616第15.1.3节 中找到更多的信息,在URI中编码敏感信息。
HiddenHttpMethodFilter
一些应用程序可以使用表单参数来覆盖HTTP方法。例如,下面的表单可以将HTTP方法视为 delete
而不是 post
。
CSRF Hidden HTTP Method Form
<form action="/process"
method="post">
<!-- ... -->
<input type="hidden"
name="_method"
value="delete"/>
</form>
重写HTTP方法发生在一个过滤器中。该过滤器必须放在 Spring Security 的支持之前。请注意,重写只发生在 post
上,所以这实际上不太可能造成任何实际问题。然而,最好的做法还是确保它被放在 Spring Security 的过滤器之前。
防御例子
Nginx防御例子
https://github.com/gartnera/nginx_csrf_prevent/blob/master/ngx_http_csrf_prevent_filter_module.c
Nginx+lua防御例子
https://gist.github.com/loveshell/7480950
参考:
How to protect your websites and web apps with anti-CSRF tokens | Invicti
XSS 与 CSRF 攻击——有什么区别? - 知乎
Cross Site Request Forgery (CSRF) :: Spring Security Reference
认识和防御 XSS 和 CSRF 攻击 | 青训营 - 掘金