CSRF

CSRF


安全 csrf 前端安全

前言:建议可以先看下 跨域 & CORS 相关的一些知识

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

一个典型的 CSRF 攻击有着如下的流程:

  • 受害者登录 a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱受害者访问了 b.com。
  • b.com 向 a.com 发送了一个请求:a.com/act=xx。
  • a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
  • a.com 以受害者的名义执行了 act=xx。
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让 a.com 执行了自己定义的操作。

几种常见的攻击类型

1. GET 类型的 CSRF

GET 类型的 CSRF 利用非常简单,只需要一个 HTTP 请求,一般会这样利用:

<img src="http://bank.com/pay?amount=10000&for=hacker" />

在受害者访问含有这个 img 的页面后,浏览器会自动向http://bank.com/pay?amount=10000&for=hacker发出一次 HTTP 请求。bank.com 就会收到包含受害者登录信息的一次跨域请求。

2. POST 类型的 CSRF

这种类型的 CSRF 利用起来通常使用的是一个自动提交的表单,如:

<form action="http://bank.com/pay" method="POST">
  <input type="hidden" name="account" value="you" />
  <input type="hidden" name="amount" value="10000" />
  <input type="hidden" name="for" value="hacker" />
</form>
<script>
  document.forms[0].submit();
</script>

访问该页面后,表单会自动提交,相当于模拟用户完成了一次 POST 操作。 POST 类型的攻击通常比 GET 要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许 POST 上面。

3. 链接类型的 CSRF

链接类型的 CSRF 并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

<a href="http://bank.com/csrf/pay.php?amount=1000&for=hacker" taget="_blank">
  重磅消息!!
  <a
/></a>

由于之前用户登录了信任的网站 A,并且保存登录状态,只要用户主动访问上面的这个 PHP 页面,则表示攻击成功。

CSRF 的特点

  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
  • 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
  • 跨站请求可以用各种方式:图片 URL、超链接、CORS、Form 提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。

CSRF 通常是跨域的,因为外域通常更容易被攻击者掌控。但是如果本域下有容易被利用的功能,比如可以发图和链接的论坛和评论区,攻击可以直接在本域下进行,而且这种攻击更加危险。

防护策略

CSRF 通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对 CSRF 的防护能力来提升安全性。 上文中讲了 CSRF 的两个特点:

  • CSRF(通常)发生在第三方域名。
  • CSRF 攻击者不能获取到 Cookie 等信息,只是使用。

针对这两点,我们可以专门制定防护策略,如下:

  • 阻止不明外域的访问
    • 同源检测
    • Samesite Cookie
  • 提交时要求附加本域才能获取的信息
    • CSRF Token
    • 双重 Cookie 验证

1. 同源检测

既然 CSRF 大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。 那么问题来了,我们如何判断请求是否来自外域呢? 在 HTTP 协议中,每一个异步请求都会携带两个 Header,用于标记来源域名:

  • Origin Header
  • Referer Header

这两个 Header 在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。 服务器可以通过解析这两个 Header 中的域名,确定请求的来源域。

使用 Origin Header 确定来源域名

在部分与 CSRF 有关的请求中,请求的 Header 中会携带 Origin 字段。字段内包含请求的域名(不包含 path 及 query)。 如果 Origin 存在,那么直接使用 Origin 中的字段确认来源域名就可以。 但是 Origin 在以下两种情况下并不存在:

  • IE11 同源策略: IE 11 不会在跨站 CORS 请求上添加 Origin 标头,Referer 头将仍然是唯一的标识。最根本原因是因为 IE 11 对同源的定义和其他浏览器有不同,有两个主要的区别,可以参考 MDN Same-origin_policy#IE_Exceptions
  • 302 重定向: 在 302 重定向之后 Origin 不包含在重定向的请求中,因为 Origin 可能会被认为是其他来源的敏感信息。对于 302 重定向的情况来说都是定向到新的服务器上的 URL,因此浏览器不想将 Origin 泄漏到新的服务器上。

使用 Referer Header 确定来源域名

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,记录了该 HTTP 请求的来源地址。对于 Ajax 请求,图片和 script 等资源请求,Referer 为发起请求的页面地址。对于页面跳转,Referer 为打开页面历史记录的前一个页面地址。因此我们使用 Referer 中链接的 Origin 部分可以得知请求的来源域名。

// 服务端代码可以这么写
if (req.headers.referer !== "http://www.xiejunyi.com/") {
  res.write("csrf 攻击");
  return;
}

这种方法并非万无一失,Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的 Referer。

如果攻击者将自己的请求这样填写,那么这个请求发起的攻击将不携带 Referer。

<img
  src="http://bank.com/pay?amount=10000&for=hacker"
  referrerpolicy="no-referrer"
/>

而且还有一种情况,当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),直接使用 referer 判断的话,也会被当成疑似 CSRF 攻击而阻止。所以在判断的时候需要过滤掉页面请求情况,此时 Header 通常符合以下情况:

Accept: text / html;
Method: GET;

但相应的,页面请求就暴露在了 CSRF 的攻击范围之中。如果你的网站中,在页面的 GET 请求中对当前用户做了什么操作的话,防范就失效了。

例如下面的页面请求:

GET https://blog.com/addComment?comment=XXX&dest=orderId

:这种严格来说并不一定存在 CSRF 攻击的风险,但仍然有很多网站经常把 GET 请求挂上参数来实现产品功能,但是这样做对于自身来说是存在安全风险的。

另外,前面说过,CSRF 大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等,统称 UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。

综上所述:同源验证是一个相对简单的防范方法,能够防范绝大多数的 CSRF 攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。

2. CSRF Token

前面讲到 CSRF 的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用 Cookie 中的信息。

而 CSRF 攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个 CSRF 攻击者无法获取到的 Token。服务器通过校验请求是否携带正确的 Token,来把正常的请求和攻击的请求区分开,也可以防范 CSRF 的攻击。

2.1 原理

CSRF Token 的防护策略分为三个步骤:

2.1.1. 将 CSRF Token 输出到页面中

首先,用户打开页面的时候,服务器需要给这个用户生成一个 Token,该 Token 通过加密算法对数据进行加密,一般 Token 都包括随机字符串和时间戳的组合,显然在提交时 Token 不能再放在 Cookie 中了,否则又会被攻击者冒用。因此,为了安全起见 Token 最好还是存在服务器的 Session 中,之后在每次页面加载时,使用 JS 遍历整个 DOM 树,对于 DOM 中所有的 a 和 form 标签后加入 Token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 HTML 代码,这种方法就没有作用,还需要程序员在编码时手动添加 Token。

2.1.2. 页面提交的请求携带这个 Token

对于 GET 请求,Token 将附在请求地址之后,这样 URL 就变成

http://url?csrftoken=tokenvalue

而对于 POST 请求来说,要在 form 的最后加上:

<input type="”hidden”" name="”csrftoken”" value="”tokenvalue”" />

这样,就把 Token 以参数的形式加入请求了。

2.1.3. 服务器验证 Token 是否正确

当用户从客户端得到了 Token,再次提交给服务器的时候,服务器需要判断 Token 的有效性,验证过程是先解密 Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个 Token 就是有效的。

这种方法要比之前检查 Referer 或者 Origin 要安全一些,Token 可以在产生并放于 Session 之中,然后在每次请求时把 Token 从 Session 中拿出,与请求中的 Token 进行比对,但这种方法的比较麻烦的在于如何把 Token 以参数的形式加入请求。

2.2.总结

Token 是一个比较有效的 CSRF 防护方法,只要页面没有 XSS 漏洞泄露 Token,那么接口的 CSRF 攻击就无法成功。 但是此方法的实现比较复杂,需要给每一个页面都写入 Token(前端无法使用纯静态页面),每一个 Form 及 Ajax 请求都携带这个 Token,后端对每一个接口都进行校验,并保证页面 Token 及请求 Token 一致。这就使得这个防护策略不能在通用的拦截上统一拦截处理,而需要每一个页面和接口都添加对应的输出和校验。这种方法工作量巨大,且有可能遗漏。

验证码和密码其实也可以起到 CSRF Token的作用,而且更安全。 这也就是为什么很多银行等网站会要求已经登录的用户在转账时再次输入密码。

在会话中存储 CSRF Token 比较繁琐,而且不能在通用的拦截上统一处理所有的接口。

那么另一种防御措施是使用双重提交 Cookie。利用 CSRF 攻击不能获取到用户 Cookie 的特点,我们可以要求 Ajax 和表单请求携带一个 Cookie 中的值。

  • 在用户访问网站页面时,向请求域名注入一个 Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw)。
  • 在前端向后端发起请求时,取出 Cookie,并添加到 URL 的参数中(接上例 POST https://back.com/pay?csrfcookie=v8g9e4ksfhw)。 后端接口验证 Cookie 中的字段与 URL 参数中的字段是否一致,不一致则拒绝。

此方法相对于 CSRF Token 就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储 Token。

当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有 CSRF Token 高,原因如下:

由于任何跨域都会导致前端无法获取 Cookie 中的字段(包括子域名之间),于是发生了如下情况:

  • 如果用户访问的网站为www.a.com,而后端的 api 域名为api.a.com。那么在www.a.com下,前端拿不到api.a.com的 Cookie,也就无法完成双重 Cookie 认证。
  • 于是这个认证 Cookie 必须被种在a.com下,这样每个子域都可以访问。
  • 任何一个子域都可以修改a.com下的 Cookie。
  • 某个子域名存在漏洞被 XSS 攻击(例如upload.a.com)。虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com下的 Cookie。
  • 攻击者可以直接使用自己配置的 Cookie,对 XSS 中招的用户再向www.a.com下,发起 CSRF 攻击。

3.2 总结

  • 无需使用 Session,适用面更广,易于实施。
  • Token 储存于客户端中,不会给服务器带来压力。
  • 相对于 Token,实施成本更低,可以在前后端统一拦截校验,而不需要一个个接口和页面添加。

3.2.2 缺点:

  • Cookie 中增加了额外的字段。
  • 如果有其他漏洞(例如 XSS),攻击者可以注入 Cookie,那么该防御方式失效。 难以做到子域名的隔离。
  • 为了确保 Cookie 传输安全,采用这种防御方式的最好确保用整站 HTTPS 的方式,如果还没切 HTTPS 的使用这种方式也会有风险。

防止 CSRF 攻击的办法已经有上面的预防措施。为了从源头上解决这个问题,Google 起草了一份草案来改进 HTTP 协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie 是个“同站 Cookie”,同站 Cookie 只能作为第一方 Cookie,不能作为第三方 Cookie,Samesite 有两个属性值,分别是 StrictLax

1. Samesite=Strict

这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外。比如说 b.com 设置了如下 Cookie:

Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3

此时在 a.com 下发起对 b.com 的任意请求,foo 这个 Cookie 都不会被包含在 Cookie 请求头中,但 bar 会,不同网站之间跳转时也是如此,举个实际的例子就是,假如淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite=Strict,那么用户从百度搜索页面甚至天猫页面的链接点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接受到那个 Cookie,而且其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。

2. Samesite=Lax

这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个 GET 请求,则这个 Cookie 可以作为第三方 Cookie。比如说 b.com 设置了如下 Cookie:

Set-Cookie: foo=1; Samesite=Strict
Set-Cookie: bar=2; Samesite=Lax
Set-Cookie: baz=3

当用户从 a.com 点击链接进入 b.com 时,foo 这个 Cookie 不会被包含在 Cookie 请求头中,但 barbaz 会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从 a.com 发起的对 b.com 的异步请求,或者页面跳转是通过表单的 post 提交触发的,则 bar 也不会发送。

总结

如果 SamesiteCookie 被设置为 Strict,浏览器在任何跨域请求中都不会携带 Cookie,新标签重新打开也不携带,所以说 CSRF 攻击基本没有机会。

但是跳转子域名或者是新标签重新打开刚登陆的网站,之前的 Cookie 都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。

如果 SamesiteCookie 被设置为 Lax,那么其他网站通过页面跳转过来的时候可以使用 Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。

总之,SamesiteCookie 是一个可能替代同源验证的方案,但还需要观望。

防御总结

  • CSRF 自动防御策略:同源检测(Origin 和 Referer 验证)。

    Referer check;请求来源限制,此种方法成本最低,但是并不能保证 100% 有效,因为服务器并不是什么时候都能取到 Referer,而且低版本的浏览器存在伪造 Referer 的风险。

  • CSRF 主动防御措施:Token 验证 或者 双重 Cookie 验证 以及配合 Samesite Cookie。

    token 验证的 CSRF 防御机制是相对来说最合适的方案。但是如果网站同时存在 XSS 漏洞的时候,这个方法也是空谈。

  • 保证页面的幂等性,后端接口不要在 GET 页面中做用户操作。

  • 验证码:强制用户必须与应用进行交互,才能完成最终请求。此种方式能很好的遏制 csrf,但是用户体验比较差。

为了更好的防御 CSRF,最佳实践应该是结合上面总结的防御措施方式中的优缺点来综合考虑,结合当前 Web 应用程序自身的情况做合适的选择,才能更好的预防 CSRF 的发生。

转自: https://juejin.cn/post/6844903689702866952 https://juejin.cn/post/6844903781704925191

© 2025 Niko Xie