Cookie与CSRF(XSRF)
2018年07月01日


本文是《Session和Cookie原理》的续篇。


在上一篇中,详细介绍了Cookie的原理。


下面介绍,如何Cookie的跨站共享,及CSRF(Cross-Site Request Forgery,跨网站请求伪造)攻击

请先阅读《HTTP访问控制(CORS)》一文,以便对跨网站请求有一个初步了解。在这篇文章中提到的一点十分关键:

“Fetch 与 CORS 的一个有趣的特性是,可以基于 HTTP cookies 和 HTTP 认证信息发送身份凭证。一般而言,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位(withCredentials = true)。”


另外,注意到,在同一个浏览器当前打开的多个Tab页网站,无论是否为同一个网站,Cookie都是共享可见的(注意:这个共享,不是说每个网站的脚本可以访问别的网站的Cookie,而是说,所有的Cookie,在浏览器发送HTTP请求认证时,都会携带过去)

(另外,1. 对于网站的JS,能否访问自己的Cookie,要看这个Cookie是否为httpOnly的。2. 如果要访问别的网站的Cookie,除非是在Cookie的domain允许的子域,否则无法访问其他网站的Cookie)

(另外,Cookie虽然是共享的,可以发送给服务器端,但是HTTP请求本身有访问控制,如果不是同一个网址发来的请求,并且服务器没有设置Access-Control-*项目的选项来允许跨站请求的话,请求会被拒绝的)


注意:“HTTP跨站请求” 和 “Cookie的跨站共享”,其实是两码事情。之所以会同时提到它们,是因为 我们经常会用到 HTTP的跨站请求,然而 HTTP的跨站请求,由于存在 Cookie的跨站共享,就可以导致 CSRF 攻击。


由于 Cookie这个共享机制,浏览器当前Tab页网站,都可以携带Cookie发送给后端服务器(对于跨域的AJAX请求,需要设置withCredentials = true,否则请求不会携带Cookie,对于非跨域的请求,或者跨域的非AJAX请求--比如通过form表单提交,浏览器会自动携带上Cookie),这样后端服务器,是无法根据Cookie来识别请求发送自哪个地方、哪个网站,有可能是网站A发出的,也可能是网站B发出的。所以当前打开的网站A可以给后端发一个请求AAA,网站B也可以发送这个请求,并且有一样的Cookie,如果只是根据Cookie来认证的话,那么从网站A和网站B发送的请求AAA都是有效的。


如果你登录网站A(网站A的后端服务器支持跨域访问)为管理员,执行了一个入库操作(http://A.com/api2),在没有退出网站A管理员身份的情况下,同时又打开了钓鱼网站B,那么网站B就可以用你的网站A管理员身份执行AJAX,操作入库等,它只要知道ajax的url即可。(如果你感兴趣,很容易就可以模拟出来这种效果,打开网站A并登录,然后打开网站B,在B的JS中执行网站A的ajax请求,甚至先打开网站B,在网站B的JS中隔1秒执行一次某个AJAX,随时等待用户登录,一旦用户登录这个AJAX操作就会执行成功)【注意,跨域请求用AJAX的话,后端服务器要设置“Access-Control-Allow-Origin”,否则不会存在CSRF攻击,且如果要携带Cookie信息的话,AJAX必须设置“withCredentials: true”,但是如果不用AJAX请求,比如使用form表单请求,则不会受到限制(亲测),下面给出我的测试方法

测试方法:

第一次,在B网站调用A系统的API:

$.getJSON('http://A.com/api2',function(result){
    console.log(result)
 });

由于是跨域的AJAX请求,而且B系统服务器端又没有设置“Access-Control-Allow-Origin”,所以报错了:

Failed to load http://A.com/api2: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.

createError.js?2d83:16 Uncaught (in promise) Error: Network Error

    at createError (createError.js?2d83:16)

    at XMLHttpRequest.handleError (xhr.js?b50d:87)

Warn:xhr.js?b50d:178 Cross-Origin Read Blocking (CORB) blocked cross-origin response http://A.com/api2 with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.


但奇怪的是,在网络请求里面可以看到这个api调用,且显示的HTTP CODE为200,但没有数据返回,应该是由于服务器端没有设置“Access-Control-Allow-Origin”,所以直接返回空的响应(response)。


第二次,我们不用AJAX请求,我们用form表单提交请求:

<script type="text/javascript">
  $(document).ready(function () {
    $("#ddd").submit();
  });
</script>
<div>
    <form id="ddd" action="http://B.com/api2" method="get" target="nm_iframe">
        <input type="text" name="keywords" />
        <input type="text" name="pageNum" value="1" />
        <input type="submit" value="Submit" />
    </form>
    <iframe id="id_iframe" name="nm_iframe" style="display:none;"></iframe>
</div>

结果不出所料,请求成功了,正常返回数据,于是我们成功模拟了一次CSRF攻击。


基于以上分析得出结论:

    大多数的CSRF攻击,都不会基于AJAX请求,而是通过form表单等请求来实现,因为这样可以不用关心服务器端是否设置了“Access-Control-Allow-Origin”。


明白CSRF原理之后,就很容易想到 防御方案:

1、服务器端,设置允许 HTTP跨站请求时,范围权限要尽量小,可以指定某站可以跨站访问,其他站点发来的请求就会被拒绝,还可以指定某个api url支持跨站请求,其他所有url都不允许跨站请求。(但是这种服务器端的设置,只能避免AJAX请求的CSRF攻击,没办法防御form表单提交的请求)

2、为了抵御来自form表单的跨站请求,服务器端应该对每次请求进行身份校验,就像JWT鉴权那样,每次请求的header中放一个token,如果没有token则拒绝请求,由于token是在本站的cookie或者localStorage中,跨站的form表单请求是拿不到token的,也就无法发起有效的请求了。(而且JWT方案不仅能防范 CSRF攻击,还能做登录、请求鉴权、身份识别的作用,但是JWT方案又会遇到 XSS 脚本攻击的可能,当然XSS也有解决方案,而且XSS攻击难度要大一些)


参考资料(写得很好,推荐阅读):

https://en.wikipedia.org/wiki/Cross-site_request_forgery

这篇参考资料的“Cookie-to-header token” 方案,就是我上面推荐的方案的一种实现方式,即把cookie里面的认证信息(比如sessionid),放一份在header中,可以存一样的,也可以HMAC签名一下。


二、Cookie跨域

上一篇文章已经说了Cookie原理及“domain-域”的概念,Cookie是不能跨域访问的,如果要想解决Cookie跨域的问题,可以另辟蹊径,达到目的,比如nginx反向代理,反向代理之后其实就是同一服务器下,不存在跨域了,还比如jsonp。本文不详细讨论,参考:https://www.cnblogs.com/1020182600HENG/p/7121148.html