Introduction
上一篇博客介绍了JWT,其中在介绍token验证时提到了CSRF攻击。在今天这篇博客,我将详细地介绍一下CSRF是什么,以及需要怎么防范。
What is CSRF
这里是我第一次介绍安全问题,首先我们大致把问题分成前端和后端,CSRF就是一个很经典的前端安全问题。我们先用一个例子去看看遇到的问题是什么?
- 同学A在使用Gmail看邮件,然后突然看到一封垃圾邮件,里面有一个链接,于是好奇地点了进去;
- 跳转到浏览器后,发现页面是空白的,好像也没有提示扣钱什么,查了一下自己的银行账户还是安全的,于是同学A就把页面关掉,觉得逃过一劫;
- 过了一个月后,同学A突然发现自己的steam账号被偷了,他很好奇对方是怎么登进去的,因为登录是steam是需要验证码的,而验证码会发送到Gmail;
- 于是同学A打开Gmail,仔细搜了下验证码的邮件,然后他发现他的发件箱出现了这封邮件,然后就发现问题了,原来他的auto filter中设置了一条转发规则,凡是遇到steam系统发送的邮件,都会自动转发一份到xxx@abc.com(攻击者的邮箱)这个邮箱;
- 然后同学A想起了一个月前发生的事情,那条链接,它又找到了那封邮件,点开那条链接,然后看了下源码,发现里面会发起一个POST请求,往自己的auto filter中添加一条转发规则。
于是事情就水落石出了,同学A首先登陆了Gmail,所以浏览器就会记录下Gmail的cookie,然后后面攻击的链接点开后,直接发起一个POST请求,请求的时候浏览器会自动带上cookie,对于Gmail来说,对cookie的验证是通过的,所以它认为这是来自用户的合法请求,于是一条转发规则就这样在用户不知情的情况下被创建了。
这个就是Gmail著名的CSRF漏洞问题:https://www.davidairey.com/google-gmail-security-hijack/
通过这里例子,我们知道了一个CSRF的攻击到底是怎么样了。然后我们从理论的角度看看CSRF是什么。
CSRF(Cross-Site Request Forgery),中文叫跨站请求伪造,指的是攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求,通过利用受害者在被攻击网站已经获取的认证凭证(cookie),绕过后台的用户认证,达到冒充用户对被攻击的网站执行某种操作的目的。
在上述的例子中,第三方网站就是那个垃圾邮件里面的链接,被攻击网站就是Gmail的服务器,受害者当然就是同学A了。所谓跨站就是从第三方网站跨到了被攻击网站。为什么是跨域的呢?因为通常来说本域下的防护会做的比较好,而外域更加有可能被攻击者掌握,所以就叫跨域。但是如果本域下有类似的漏洞,那就会带来更加严重的问题。
Characteristics of CSRF
- 攻击的发起方一般是第三方网站,而被攻击的网站无法做防护,因为仅凭cookie根本分不清是不是用户的真实操作;
- 攻击的手段一般是冒充用户进行提交操作,而不是直接窃取数据,因为浏览器返回的结果是在用户的界面上,根据浏览器的同源策略的限制,攻击者也无法获取返回的结果。因此CSRF重点的防护对象是允许用户操作的界面,而对于读请求则风险较低;
- 整个攻击过程,攻击者都无法获取用户的身份凭证,他只是冒用,而没有窃取;
How to deal with CSRF
我们了解了CSRF的攻击原理后,就来看看怎么对应用做一个基本的防护。
- 阻止不明外域的访问
- 访问是也进行同源检测;
- 使用samesite cookie;
- 提交操作请求时,要求带上本域信息
- CSRF token;
- 双重cookie验证;
同源检测
既然CSRF攻击的一大特点是从外域进行访问,那么可以直接在访问的时候就判断origin是否来自外域。这里又有一个问题,服务器怎么知道是来自哪个域呢?我们要确保这个信息是可靠的,才能做到同源检测。
通常在http协议中,每个异步请求都会携带两个Header,用于标记来源域名:
- Origin Header
- Referer Header
这两个header在浏览器发起请求时会自动带上,并且不能由用户自定义内容填写,所以服务器是可以利用这两个header的域名来知道请求的来源域。接下来我们就来看看怎么利用这两个header去做检测。
Origin Header
在部分与CSRF有关的请求中(注意是与CSRF有关的请求,正常的同域名的请求不会带上这个header,F12可以查看),请求的Header会带上Origin字段,里面包含了请求的域名。服务端可以直接利用这个信息去做同源检测。
但有三种情况Origin字段不存在:
- IE11同源策略:IE11不会在跨站CORS请求上添加Origin,根本原因是IE11对same-origin的定义不同。IE认为如果跨域请求发生在不同端口之间也属于同源,则不会添加Origin字段;同时对于高度信任的域,IE也不会apply同源策略;
- 302重定向:302重定向时不会带上Origin,因为Origin被认为是敏感信息,因此浏览器不会把这类信息泄漏到新的服务器上(重定向的服务器)。
Referer Header
除了使用Origin字段外,还能使用Referer这个字段,它记录了该http请求的来源地址。这个来源地址,对于ajax请求、资源请求来说,就是发起请求时页面的地址;对于页面跳转来说就是上一个页面的地址。
但是这个Referer的值是由浏览器提供的,严格来说也不是绝对安全,具体要看每个浏览器的实现。因此在2014年W3C发布了Referrer Policy草案,对浏览器如何发送Referer做了详细的规定,目前大部分的浏览器都遵循这份草案。目前来说,只要把Referrer Policy设置为same-origin,则符合严格的同源校验要求。
这里要注意当https跳转到http页面时,Referer会丢失。
对于Origin和Referer都不存在的请求,最保险的做法就是都拒绝掉,但是这里会存在一种误伤。如果用户从搜索引擎搜到某个页面,然后点开,这样大概率是不同源的,那么如何过滤掉这种请求呢?
通常来说这种页面请求的Header符合以下特性:
1 | Accept: text/html |
这种Header意思是客户端向服务端请求html资源,使用的是GET方法,因此大概率就是普通的页面访问了。所以一般的网站会把这类请求放开,这也说明对于这类请求,CSRF是没有同源检测的。反过来说,对开发者而言,不要在GET的方法中去做用户数据的操作,最好就是只返回一些数据(许多网站为了减少请求次数,会把主文档GET请求带上参数实现一下产品功能,实际上这是一个风险点)。
CSRF token
上面使用同源检测的方式实际上还是大家都遵循某个规范,然后服务端信任浏览器传过来的Header,从而进行判断。如果是使用主流的、新版的浏览器就一般没什么问题,但是如果遇到不遵守规范的浏览器就很容易gg了。此时,作为一个成熟的开发者,当然不能把锅甩给用户,就算用户使用了低版本的浏览器,我们同样也需要做好防护。因此CSRF是一种更加彻底的手段。
重新理解CSRF的原理,是借用了cookie信息,但是从头到尾攻击者都无法知道用户的cookie具体是什么。所以我们只需要再携带多一个信息,这个信息不能在cookie中,必须是攻击者无法获取的,这样服务端就能利用这个额外的信息去做身份校验,这个额外的信息就是CSRF token。
CSRF的验证过程主要有以下步骤:
- 页面加载时,遍历整个DOM tree,对其中所有的
a
和form
标签添加token,这样在请求这些资源时给服务器验证。但是对于动态的HTML代码,需要在coding中加入token; - GET请求会把token放在URL后面,而POST请求则放在form中;
- 服务器收到请求后,拿到token,需要判断有效性。服务器生成token一般是通过一个随机生产的字符串加上时间戳组成一个新的字符串,然后加密。所以验证的过程相反,先对token进行解密,然后拿到加密的字符串和时间戳,比对加密的字符串是否一致,同时确保时间还没过期,如果两者都满足要求则验证通过。
一般来说服务端会先生成一个csrf token返回给客户端,然后带上一个refresh token,当经过一定时间后,csrf token会过期,需要客户端重新请求,带上用户的登录信息和refresh token,去换取新的csrf token。这样通过过期的方法一定程度上提高了csrf的安全性。
虽然CSRF token方法使用起来比同源检测要靠谱得多,但是同时也给服务端带来了压力。这个压力具体指的是,用户的session一般来说会存在单台设备上,或者是partition的存储组件上,如果每次都需要全部扫描一般的话,耗时将会是不可接受的。因此目前的做法是采用Encrypted Token Pattern,这种方式下,CSRF token是由user_id(或其他有业务意义的值)加上时间戳以及随机数再加密生成,这样在解密验证时,第一层解密后就能直接拿到user_id,然后路由到相应的服务器进行处理,实现分布式。
Summary
这篇博客从一个实际的例子出发介绍了CSRF的过程及背后的原理,也简单介绍了两种防范的方法。