什么是CORS
CORS全称Cross-origin Resource Sharing,翻译过来就是跨域资源共享。
跨域资源共享(CORS) 是一种机制,它允许浏览器向跨源(协议 + 域名 + 端口)服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
简单来说,其实CORS就是一种可以放宽浏览器同源策略的机制,可以通过浏览器让不同的网站和不同的服务器之间通信。
什么情况下需要CORS?
其实上面已经提到一些了,这里再做一下总结。
前文提到的由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
Web字体 (CSS 中通过@font-face使用跨域字体资源)
WebGL贴图
使用 drawImage 将 Images/video 画面绘制到 canvas
样式表(使用 CSSOM)
功能概述
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(尤其是除了 GET 、head等类似简单请求以外的 HTTP 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
这一段可能看起来有点绕,下面我们将一一阐述。
工作原理
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请求才能成功执行。
漏洞怎么产生
上面我们已经知道了,对于附带身份凭证的请求(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的攻击手法,来泄露用户的隐私数据和敏感数据。
关于CORS漏洞的防御
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