CSRF 指北

CSRF(Cross-site request forgery),跨站请求伪造经验总结。

原理

CSRF 最重要的原理是 Web 的 Cookies 隐式身份验证机制。Web 的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的,故 CSRF 攻击的一般是由服务端解决。

常见的利用场景如下:

  • 合法网站 S1:https://www.tr0y.wang
  • 合法用户 U1:Tr0y,受害者
  • 非法网站 S2:https://www.hackit.com
  • 非法用户 U2:Hacker,攻击者

背地操作

Tr0y 在 https://www.tr0y.wang 部署着自己的博客,为了管理的方便,在后台开了一个接口:https://www.tr0y.wang/delete?id=4382。只要 Tr0y 登陆后,访问这个 url,就能把 id 为 4382 的文章删除。

Tr0y 博客的部署方式被 Hacker 得知后,他发了一份钓鱼邮件给 Tr0y,诱使他点击一个 url。毫无防备的 Tr0y 点击了它,打开一看却是空白的页面,一脸疑惑的 Tr0y 关了页面,很快忘了这件事。

在这个过程的背后发生了什么呢?Hacker 构造了非法网站 S2,并在其中嵌入了恶意的代码:

1
<img style="width:0;" src="https://www.tr0y.wang/delete?id=4382" />

Tr0y 打开页面后,浏览器会自动加载 img,即访问了这个 url。由于之前 Tr0y 登录过自己的博客,所以浏览器这时一看要访问https://www.tr0y.wang/delete?id=4382,便帮 Tr0y 带上了 Cookies。博客的后台一看 Cookies,这个操作的确经过认证,便放行了,导致文章被删除。而这时 Tr0y 却毫不知情。

假设这里的删除博文的链接改为银行转账,危害就更大了。

蠕虫

假设某交友网站有发私信的接口与查询好友的接口(接口需利用跨域的形式,例如 jsonp,否则拿不到数据),利用 CSRF 即可形成蠕虫,步骤如下:

  1. Hacker 构造恶意页面,页面中嵌入恶意代码
  2. 寻找某个受害用户作为第一个传播点,诱使他点击恶意页面,触发蠕虫的传播。
  3. 蠕虫查询出的受害用户的所有好友,并将恶意页面放在私信中,发给这些好友
  4. 受害用户的好友打开恶意页面,导致恶意页面通过私信发送给受害用户的好友的好友,完成蠕虫的传播阶段

实际上,和其他形式的蠕虫一样,CSRF 蠕虫主要做了 2 件事:

  • 获取更多的传播路径
  • 触发下一步的传播

防御

攻击条件

回顾上面的攻击过程,其实需要有以下几个必备条件:

  1. U1 在 S1 处于登录状态
  2. 浏览器对 S1 发起访问的时候自动带上 S1 的 Cookies
  3. S1 对请求不经过进一步验证
  4. U1 访问 S2

(如果攻击无需 Cookies 的话,仅需要 3、4 两个条件)

既然 CSRF 危害这么大,那么各方都做出了什么努力来保证安全呢?主要还是围绕 2、3 两个条件进行防御,毕竟第一、四个条件太正常了,必须满足。

浏览器防御

浏览器的防御机制主要是针对 Cookies 的保护,此时的防御阵地在第二个条件。

首先要从浏览器的 Cookies 说起,有 2 种:

  • Session Cookie,即临时 cookies
  • Third-party Cookie:即本地 cookies

它们的区别在于,Third-party Cookie在服务器Set-Cookie时指定了 Expire 时间,只有到
了 Expire 时间后 Cookie 才会失效,所以这种 Cookie 会保存在本地;而 Session Cookie则没有指定 Expire 时间,保存在浏览器进程的内存中,所以浏览器关闭后就失效了,但是在浏览器进程的生命周期内,即使浏览器新打开了 Tab 页,Session cookie 也是有效的。

如果浏览器从一个域的页面中,要加载另一个域的资源,由于安全原因,某些浏览器会阻止 Third-party Cookie 的发送。

在当前的主流浏览器中,默认会拦截 Thid-party Cookie 的有:

  • IE 6、7、8、Safari

不会拦截的有:

  • Firefox 2、3
  • Opera
  • Google Chrome
  • Android

但是,如果 CSRF 攻击的条件不需要使用 Cookie,那么这些限制也就没用了。虽然部分利用场景无需经过认证,但是这个情况相对少见,大部分重要操作还是需要经过认证的。所以浏览器拦截第三方 Cookies 的发送在一定程度上的确缓解了 CSRF 的攻击,但是不咋地靠谱,毕竟你没法要求用户只使用 IE。

再者,P3P 的出现使得浏览器的防御变得更加不靠谱了。P3P 全称 Platform for Privacy Preferences,隐私设定平台规范。这个规范极其复杂,简言之,就是网站向浏览器声明自己的隐私政策,比如网站是否搜集访问者的个人信息,设置 cookie 的用途等等。浏览器会依据设置,决定在第三方对这个网站进行请求时,是否接受网站的 set-cookie 与是否发送此网站的 Cookies。其实这两个是有关联的,如果能 set-cookie 却不能发送,那 set 有何用呢?

如果网站返回给浏览器的 HTTP 头中包含有 P3P 头,将允许浏览器发送第三方 Cookie。在 IE 下即使是 <iframe><script> 等标签也将不再拦截第三方 Cookie 的发送。在网站的业务中,P3P 头主要用于类似广告等需要跨域访问的页面。但是 P3P 头设置后,对于 Cookie 的影响将扩大到整个域中的所有页面,因为 Cookie 是以域和 path 为单位的。

所以,仅仅依赖浏览器来防御 CSRF 是相当不靠谱的

开发防御

求人不如求己,让开发人员来防御 CSRF 可行么?即将防御阵地转移至第三个条件,既然 CSRF 是在用户不知情的情况下发起的请求,那么强制用户进行交互不就解决了吗?

强制用户交互

不能利用 POST 防御

由于大多数讲 CSRF 的例子都以 GET 型为例,让人以为重要的操作改为 POST 型就不能进行利用了,实际上是让用户点击按钮,也算强制交互吧。

这个观点是相当错误的,以 GET 型为例只不过是为了最简化地讲清楚攻击原理罢了,js 能够控制前端的操作太多了。

这个防御有 2 个主要的问题:

  1. 有些后台不会精确区分 GET 型与 POST 型。例如 PHP 的$_REQUEST
  2. 未达到强制交互的目的(构造 POST 请求也不难):
    1
    2
    3
    4
    5
    6
    7
    8
    <form action="https://www.tr0y.wang/delete" id="api" method="post" >
    <input type="text" name="id" value="4382" />
    <input type="submit" name="submit" value="submit" />
    </form>
    <script>
    var f = document.getElementById("api");
    f.submit();
    </script>

    然后再将这个 iframe 隐藏起来,那么对于打开恶意网站的 Tr0y 来说,依旧是被删了博文却毫无所知。

所以,利用 POST 来防御 CSRF 是不可行的

验证码

验证码被认为是对抗 CSRF 最简洁有效的防御方法,验证码类型也日新月异,强度不断升级:混合符号、数字计算、物品判断、滑动解锁...

验证码显然需要强制用户进行交互,所以能够很好地抵御 CSRF 攻击。唯一不足的地方就是它太强制了,很多时候处于用户体验的考虑,无法给所有的操作都加上验证码。

所以,验证码防御 CSRF 只能作为一种辅助手段,无法大量部署。

二次验证

强制用户交互的手段相当地好使,可惜降低了用户体验。之所以需要用户交互,无非是为了验证的确是用户发起的请求,拓宽一下这个思路,便是二次验证防御思路。

检查请求来源

检查请求来源的主要方式为 Referer、Origin,就是 HTTP 请求头的那个(两者比较类似,下面仅以 Referer 为例)。Referer 最常见的应用实际上是防盗链。比如我的博客就有。防盗链利用了 referer 检查请求的来源,同样也可以利用来防御 CSRF。

比如,正常情况下,我要删除一篇博客,首先需要登入到后台,然后再点击“删除”。此时,我的 referer 肯定就是后台地址。那么如果有一天,我的服务器收到了删除的请求,referer 却是https://www.hackit.com,那肯定不正常,直接拒绝请求。这个方法无需用户交互,也很便利。

可惜的是,服务器并非在所有情况下都能获得 referer:

  • 用户为了保护隐私,自己阻止了浏览器发送 referer
  • 攻击者可以让自己的页面不发送 referer:<meta name="referrer" content="never">这个代码告诉浏览器,所有从当前页面中发起的请求都不要携带 referer
  • HTTPS 转为 HTTP 的时候,浏览器出于安全的考虑,不发送 referer。例如网站中有<meta name="referrer" content="default">,如果当前页面使用的是 HTTPS,而正要加载的资源使用的是 HTTP,则将 HTTP header 中的 referer 置空。但是如果是 content="always" 的话,浏览器会把 HTTPS 的 referer 带给 HTTP 的页面。

顺便说一句,2014 年,W3C 的 Web 应用安全工作组发布了 Referrer Policy 草案,对浏览器该如何发送 Referer 做了详细的规定:

上面总结起来就是,CSRF 发生的时候 referer 肯定不对,但是 referer 不对的时候不一定出现了 CSRF。

(在 302 重定向之后 Origin 也不包含在重定向的请求中,因为 Origin 可能会被认为是其他来源的敏感信息。对于 302 重定向的情况来说都是定向到新的服务器上的 URL,因此浏览器不想将 Origin 泄漏到新的服务器上。)

就算成功获得了,验证 referer 的代码也要写好,别被钻空子了。最后还有个问题是,基于来源判断来防御 CSRF 不能防御站内发起的攻击,例如论坛,攻击者有权限在发布评论(含链接、图片等,统称 UGC)的地方插入恶意链接诱使受害者点击,那么它可以直接在本域发起攻击(虽然没有“跨站”,也算 CSRF 吧)。

所以,依赖 Referer、Origin 也是不可行的,它也是只能作为一种辅助的防御,也可以用于寻找 CSRF 发生的痕迹。

Token

再再再拓宽一下思路,referer 的防御思路其实是增加一个攻击者无法伪造的东西。那么我们可以在请求头或者参数中增加一个 Token,这个 Token 只有服务器知道如何生成,那么它就能代替 referer 作为不可伪造的东西,从而防御 CSRF。

防御方案呼之欲出,这边是 Anti CSRF Token

示例:
将删除接口 https://www.tr0y.wang/delete?id=4382,改为https://www.tr0y.wang/delete?id=4382&token=f07a7a0390c4a7a29ec9ea30bac80606

当然,这个 token 的值是随机的,满足不可预测性。至于 Token 如何给用户,可以通过 cookies 来设定。Token 需要同时放在表单和 cookies 中。在提交请求时,服务器只需验证表单中的 Token 与用户 Cookies 中的 Token 是否一致,如果一致,则认为是合法请求;如果不
一致,或者有一个为空,则认为请求不合法,可能发生了 CSRF 攻击。

需要注意的是,token 要及时销毁,且不能泄露。比如 token 作为 get 参数出现在 url 里,那么可能会被 referer 泄露出去。例如:
https://www.tr0y.wang/manage?id=4382&token=f07a7a0390c4a7a29ec9ea30bac80606是博客后台管理地址,Tr0y 在这里点击删除按钮时,js 会将 url 里的 token 取出,放入表单中一起提交。如果在这个页面中攻击者可以放入自己服务器的图片,那么 Tr0y 在打开这个后台管理页面的时候,浏览器会请求加载这个图片,这时攻击者就能看到图片的 referer 中的 token,从而进行 CSRF。

因此在使用 Token 时,应该尽量把 Token 放在表单中,把敏感操作由 GET 改为 POST,以 form 表单的形式提交,可以避免 Token 泄露。当然,放在请求头里也阔以...只不过一般用于 AJAX 中(毕竟要加请求头),有较大的局限性。

另外,当网站同时存在 XSS 与 CSRF 时,这个方案也是无效的。因为 XSS 可以模拟浏览器执行任意操作,攻击者可以请求页面后,读出页面内容里的 Token 值,然后再构造出一个合法的请求。这个过程称之为 XSRF,实际上是 XSS 与 CSRF 的组合技。但是,这个场景并不影响这个方案的有效性,防御 CSRF 的方案就是应该解决 CSRF 而不是 XSS,XSS 问题应该由 XSS 的防御方案来解决。

最后,这个方案也有不足的地方。

如果 Token 保存在 cookies 里,那么如果用户打开了 2 个页面,这个时候两个页面的 cookie 与 token 均相同。当用户提交了 A 页面后,服务器便销毁 token。然后当用户提交 B 页面时,token 就与 cookies 不一致了。

如果采用 Session 的方式,又会导致服务器负载加重,且在分布式的系统里,如何共享也是需要考虑的。

于是还有人提出了 双重 Cookie 验证Samesite CookieJWT 等等来升级这个防御方案。详细的讨论可以看美团技术团队在 freebuf 发的文章,我的博客也有笔记:传送门🚪

总得来说,现在业界针对 CSRF 防御的常见方案就是 Token,Token 的生成、放置的位置、验证的方式可能会根据实际情况进行调整,但是二次认证的思路基本没变。

用户防御

作为用户,我们可以做些什么呢?

  • 使用网页版邮件的浏览邮件或者新闻也会带来额外的风险,因为查看邮件或者新闻消息有可能导致恶意代码的攻击。
  • 尽量不要打开可疑的链接,一定要打开时,使用不常用的浏览器。
  • 及时手动退出登录,尤其是那些重要的网站

来呀快活呀


CSRF 指北
https://www.tr0y.wang/2019/06/02/CSRF指北/
作者
Tr0y
发布于
2019年6月2日
更新于
2024年4月19日
许可协议