浏览器安全的基石是”同源政策”(same-origin policy)。很多开发者都知道这一点,但了解得不全面。
本文详细介绍”同源政策”的各个方面,以及如何规避它。
一、概述
1.1 含义
1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。
最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页”同源”。所谓”同源”指的是”三个相同”。
- 协议相同
- 域名相同
- 端口相同
举例来说,http://www.example.com/dir/page.html
这个网址,协议是http://
,域名是www.example.com
,端口是80
(默认端口可以省略)。它的同源情况如下。
http://www.example.com/dir2/other.html
:同源http://example.com/dir/other.html
:不同源(域名不同)http://v2.www.example.com/dir/other.html
:不同源(域名不同)http://www.example.com:81/dir/other.html
:不同源(端口不同)
1.2 目的
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,”同源政策”是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
1.3 限制范围
随着互联网的发展,”同源政策”越来越严格。目前,如果非同源,共有三种行为受到限制。
(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,我将详细介绍,如何规避上面三种限制。
二、Cookie
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain
共享 Cookie。
举例来说,A网页是http://w1.example.com/a.html
,B网页是http://w2.example.com/b.html
,那么只要设置相同的document.domain
,两个网页就可以共享Cookie。
1 |
document.domain = 'example.com'; |
现在,A网页通过脚本设置一个 Cookie。
1 |
document.cookie = "test1=hello"; |
B网页就可以读到这个 Cookie。
1 |
var allCookie = document.cookie; |
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com
。
1 |
Set-Cookie: key=value; domain=.example.com; path=/ |
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
三、iframe
如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe
窗口和window.open
方法打开的窗口,它们与父窗口无法通信。
比如,父窗口运行下面的命令,如果iframe
窗口不是同源,就会报错。
1 2 |
document.getElementById("myIFrame").contentWindow.document // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame. |
上面命令中,父窗口想获取子窗口的DOM,因为跨源导致报错。
反之亦然,子窗口获取主窗口的DOM也会报错。
1 2 |
window.parent.document.body // 报错 |
如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain
属性,就可以规避同源政策,拿到DOM。
对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题。
- 片段识别符(fragment identifier)
- window.name
- 跨文档通信API(Cross-document messaging)
3.1 片段识别符
片段标识符(fragment identifier)指的是,URL的#
号后面的部分,比如http://example.com/x.html#fragment
的#fragment
。如果只是改变片段标识符,页面不会重新刷新。
父窗口可以把信息,写入子窗口的片段标识符。
1 2 |
var src = originURL + '#' + data; document.getElementById('myIFrame').src = src; |
子窗口通过监听hashchange
事件得到通知。
1 2 3 4 5 6 |
window.onhashchange = checkMessage; function checkMessage() { var message = window.location.hash; // ... } |
同样的,子窗口也可以改变父窗口的片段标识符。
1 |
parent.location.href= target + "#" + hash; |
3.2 window.name
浏览器窗口有window.name
属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。
父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入window.name
属性。