本文作者:晚风(来自信安之路作者团队)
做前端开发经常会碰到各种跨域问题,通常情况下,前端除了 iframe 、script 、link、img、svg 等有限的标签可以支持跨域外(这也与这些标签的用途有关),其他的资源都是禁止跨域引用的。说到跨域,与浏览器的同源策略是密不可分的。那我们先来理解一下浏览器为什么要设置同源策略。
首先,同“源”的源不单单是指两个页面的主域名,还包括这两个域名的协议、端口号和子级域名相同。举个例子,假设我现在有一个页面http://www.a.com/index.html
,域名是 www.a.com
,二级域名为 www
,协议是 http
,端口号是默认的 80
,这个页面的同源情况如下:
同源策略存在的意义就是为了保护用户的信息的安全。一般网站都会把关于用户的一些敏感信息存在浏览器的 cookie 当中试想一下,如果没有同源策略的保护,那么 b 页面也可以随意读取 a 页面存储在用户浏览器 cookie 中的敏感信息,就会造成信息泄露。如果用户的登录状态被恶意网站能够随意读取,那后果不堪设想。由此可见,同源策略是非常必要的,可以说是浏览器安全的基石。
除了 cookie 的访问受到同源策略的限制外,还有一些操作也同样受到同源策略的限制:
(1) 无法读取非同源网页的 Cookie 、sessionStorage 、localStorage 、IndexedDB
(2) 无法读写非同源网页的 DOM
(3) 无法向非同源地址发送 AJAX请求(可以发送,但浏览器会拒绝响应而报错)
虽然所有的页面都有浏览器的同源策略的保护,但我们仍然有一些办法绕过浏览器的同源策略限制。下面我总结一下目前已知的几种跨域方法和可能会导致的安全问题。如果有什么其他的方法,欢迎在下面留言补充。
1、JSONP
JSONP(json with padding) ,利用了 <script>
标签能跨域的特性。需要前端和后端约定好一个函数名,当前端请求后端时,返回一段 JS。这段 JS 调用了之前约定好的回调函数,并将数据当作参数传入,完成数据的跨域传递。就这样看文字可能有点难以理解,我们来看一个例子:
需要获取数据的页面 http://a.com/index.html
:
<script> function foo(data) { console.log(data.txt); }</script><script src="http://b.com/api?callback=foo"></script>
访问非同域的页面 http://b.com/api?callback=foo
,返回数据如下:
foo({ txt : 'example' })
在这个例子中,约定好的回调函数名为 foo
,由 callback 参数告知服务器要调用哪个函数来传递数据,也就是要传递什么数据。然后服务器就会返回相应的数据给页面,从而实现了数据的跨域传输。这就是整个 JSONP 的流程。由于这种方式是利用了 script 标签的特性来实现跨域,所以只能用 GET 方式获取数据。
这种方式有几个安全问题:
一是接收请求的 api 页面是属于公共使用的那还好,如果是内部私用就会造成一个用户信息泄露的问题;
二是如果 callback 参数的内容是一段 js 代码,当后端没有过滤或者过滤不严时,可能会出现 XSS 问题;三是有可能会出现 SOME 漏洞。
2、document.domain
这种方式有些局限性,只能在顶级域名相同的两个页面中跨域访问。通常情况下,在一个页面中内嵌一个不同域的 iframe,两个页面是无法相互访问的,所以多用于控制页面中内嵌的 iframe。然后再来说下 js 中的 document.domain 这个东西。这个东西会显示当前页面的域名。也可以设置,但有限制,只能设置成当前的域名或者顶级域名。如果设置为其他的域名则会报一个“参数无效”的错误:
document.domain //www.a.comdocument.domain = 'www.a.com' //www.a.comdocument.domain = 'a.com' //a.comdocument.domain = 'b.com' //参数错误
举个例子,现在有两个页面,http://www.a.com
和 http://a.com
,这两个页面是不同域的。前一个域名的 document.domain 是 www.a.com
,后一个域名的 document.domain 是 a.com
。在前一个页面中引入后一个页面,并把前一个页面的 document.domain 改为 a.com
,那么这两个页面就能相互操作了,也就是实现了同一顶级域名之间的跨域。
3、window.name
关于 window.name 我们先来看这样一个场景,首先是 A 站:
window.name = 'A site data';location.href = 'http://b.com';
设置完 window.name 后自动跳转到 B 站,此时我们在控制台里 alert 出 window.name ,发现还是我们在 A 站中设置的数据。可以看到如果在一个标签里跳转页面的话,我们的 window.name 是不会改变的。我们可以了利用这个特性进行跨域。顺便提一下,从页面内部打开的另一个页面会包含前一个页面的引用,这也是 target="_blank" 漏洞的成因。
相同的技术也可以用在 iframe 的跨域数据传递中。
这是含有数据的页面:
这是取数据的页面:
然后浏览器不给情面地给了一个报错...
我不就是跨了个域嘛,就给我一个大大的报错。没事,我们有 JS 黑魔法。对代码进行少许改动,在 iframe 加载完后立即把它的 src 改为同域,这样就可以读取 iframe 的 window.name 了。在实际开发中也可以不设置为同域,而设置为 about:blank
,因为这个页面中包含了同域的引用,而且因为是空白页面,可以提高页面加载速度。
成功跨域拿到了数据:
在安全中,我们一般使用 window.name 来缩短 XSS 的 payload。
4、postMessage
postMessage 是 HTML5 时代新出现的 API。用于安全的进行跨域请求。从字面意思就可以想象,就是一个页面对另一个页面发消息来实现跨域通信。下面是最简单的一个例子:
A 站是发送数据端:
<iframe src="http://b.com" id="com_b"></iframe>...<script> document.getElementById('com_b').contentWindow.postMessage('hello b','*')</script>
B 站是接收数据端:
<script> window.addEventListener('message',function(event){ alert(event.data); },false);</script>
以上就是 postMessage 的最简单的一个 Demo。这套跨域机制本身是没有问题的,安全问题都出在人身上。
主要有两个地方容易出问题:
一是 A 站作为消息发送端,如果对消息的保密性有要求,那就不能把接收端写为 *
,需要指定接收消息的域;
二是 B 作为数据接收端,需要在处理接收到的数据之前对数据的来源做验证,防止来源被恶意伪造,从而页面被注入恶意数据。所以推荐使用以下的做法:
<script> window.addEventListener('message',function(event){ var origin = event.origin || event.originalEvent.origin; //兼容处理 if(origin !== 'http://a.com') return; handle(event.data); },false);</script>
5、browser SOP bug
虽然所有的浏览器都有同源策略,但是各家浏览器实现的方式也是各不相同。难免实现也会有漏洞。我们可以找出浏览器同源策略的漏洞来实现跨域访问。例如浏览器对 CSS(层叠样式表)的松散解析就会导致跨域bug的出现。可以看这个例子:
https://bugs.chromium.org/p/chromium/issues/detail?id=9877
1、反代服务器
由于服务器是可以跨域访问数据的。于是我们前端想要什么别的域的数据直接告诉后端服务器,让服务器帮我们去跨域读数据,获取到了再传给我们,这样前端也可以处理别的域内的数据了。也就是说,将其他域名的资源映射到你自己的域名之下,这样浏览器就认为他们是同源的。这就是反代服务器的原理,是不是非常简单。一般我们常常使用 nginx 来配置反向代理服务器。
2、CORS
CORS 设置起来非常简单。只要在相关页面返回一个 “Allow-Control-Allow-Origin” 的响应头即可。类似:
header("Allow-Control-Allow-Origin: http://www.a.com")
具体可以看阮一峰老师的这篇文章:
http://www.ruanyifeng.com/blog/2016/04/cors.html
CORS 跨域这种方式更像是 JSONP 的升级版,像是给 JSONP 加了一个白名单。只有服务器白名单中的请求才能被正确的响应。
在本届 DEFCON 大会上也提到了这种跨域方式的不安全性。其实大多数问题还是出在没有正确配置 ACAO
响应头上,如果直接设为 *
,这就相当于直接把浏览器的同源策略去掉了,所有的域都能访问这个域的文件了。这边提供给了一个 CORS 跨域扫描器,用来检测网站的 CORS 配置是否安全。
https://github.com/chenjj/CORScanner
3、flash
对 flash 跨域的了解不多。这项技术在网页方面也在没落。这里就简单地提一下。假设 A 站要跨域访问B站,首先会检查 B 站下的 crossdomain.xml 文件,如果没有,则访问不成功。如果有,且里面设置了允许 A 站点访问,那么 AB 站点就可以跨域通信了。下面是一个 crossdomain.xml 文件的例子:
<?xml version="1.0"?><cross-domain-policy> <allow-access-from domain="*" /></cross-domain-policy>
如果不想域内的文件被其他任何域都能访问到,那么这种做法是不推荐的。正确的做法应该是明确指定本域内的文件能被哪些域访问。