浏览器同源政策及其规避方法

作者: 阮一峰

日期: 2016年4月 8日

珠峰培训

浏览器安全的基石是"同源政策"(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。


document.domain = 'example.com';

现在,A网页通过脚本设置一个 Cookie。


document.cookie = "test1=hello";

B网页就可以读到这个 Cookie。


var allCookie = document.cookie;

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。

另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com


Set-Cookie: key=value; domain=.example.com; path=/

这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。

三、iframe

如果两个网页不同源,就无法拿到对方的DOM。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。

比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。


document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父窗口想获取子窗口的DOM,因为跨源导致报错。

反之亦然,子窗口获取主窗口的DOM也会报错。


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。如果只是改变片段标识符,页面不会重新刷新。

父窗口可以把信息,写入子窗口的片段标识符。


var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

子窗口通过监听hashchange事件得到通知。


window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

同样的,子窗口也可以改变父窗口的片段标识符。


parent.location.href= target + "#" + hash;

3.2 window.name

浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。

父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入window.name属性。


window.name = data;

接着,子窗口跳回一个与主窗口同域的网址。


location = 'http://parent.url.com/xxx.html';

然后,主窗口就可以读取子窗口的window.name了。


var data = document.getElementById('myFrame').contentWindow.name;

这种方法的优点是,window.name容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name属性的变化,影响网页性能。

3.3 window.postMessage

上面两种方法都属于破解,HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。

这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

举例来说,父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage方法就可以了。


var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

子窗口向父窗口发送消息的写法类似。


window.opener.postMessage('Nice to see you', 'http://aaa.com');

父窗口和子窗口都可以通过message事件,监听对方的消息。


window.addEventListener('message', function(e) {
  console.log(e.data);
},false);

message事件的事件对象event,提供以下三个属性。

  • event.source:发送消息的窗口
  • event.origin: 消息发向的网址
  • event.data: 消息内容

下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息。


window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
}

event.origin属性可以过滤不是发给本窗口的消息。


window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;
  if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
  } else {
    console.log(event.data);
  }
}

3.4 LocalStorage

通过window.postMessage,读写其他窗口的 LocalStorage 也成为了可能。

下面是一个例子,主窗口写入iframe子窗口的localStorage


window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') {
    return;
  }
  var payload = JSON.parse(e.data);
  localStorage.setItem(payload.key, JSON.stringify(payload.data));
};

上面代码中,子窗口将父窗口发来的消息,写入自己的LocalStorage。

父窗口发送消息的代码如下。


var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');

加强版的子窗口接收消息的代码如下。


window.onmessage = function(e) {
  if (e.origin !== 'http://bbb.com') return;
  var payload = JSON.parse(e.data);
  switch (payload.method) {
    case 'set':
      localStorage.setItem(payload.key, JSON.stringify(payload.data));
      break;
    case 'get':
      var parent = window.parent;
      var data = localStorage.getItem(payload.key);
      parent.postMessage(data, 'http://aaa.com');
      break;
    case 'remove':
      localStorage.removeItem(payload.key);
      break;
  }
};

加强版的父窗口发送消息代码如下。


var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
// 存入对象
win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
// 读取对象
win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
window.onmessage = function(e) {
  if (e.origin != 'http://aaa.com') return;
  // "Jack"
  console.log(JSON.parse(e.data).name);
};

四、AJAX

同源政策规定,AJAX请求只能发给同源的网址,否则就报错。

除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。

  • JSONP
  • WebSocket
  • CORS

4.1 JSONP

JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。

它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

首先,网页动态插入<script>元素,由它向跨源网址发出请求。


function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

上面代码通过动态添加<script>元素,向服务器example.com发出请求。注意,该请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的。

服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。


foo({
  "ip": "8.8.8.8"
});

由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。

4.2 WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

下面是一个例子,浏览器发出的WebSocket请求的头信息(摘自维基百科)。


GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。

正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。


HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

4.3 CORS

CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。

下一篇文章,我会详细介绍,如何通过CORS完成跨源AJAX请求。

(完)

一灯学堂

优达学城

留言(45条)

鞭辟入里;买了一本《如何变得有思想》准备拜读

很好的文章,关键点很全,通俗易懂,学习了。
感谢!

postMessage那个, 试了一下, 似乎不好使啊, 代码如下:

源window是: https://www.google.com.hk

window.addEventListener('message', function(event) {
console.info(event);
}, false);
var popup = window.open('http://baidu.com', 'title');
popup.postMessage('Hello World!', 'https://www.google.com.hk');

太棒了

关于cookie那段并不完全正确。设置cookie时是可以指定域名的。在 a.testdomain.com/testpage.html 页面中设置域名为 testdomain.com (一级域名)的cookie,该域名的所有二级三级etc域名下的页面都能读取到该cookie,不需要做任何设置。

@shukebeta:谢谢指出,已经加进文章了。

@ybq:没错。postMessage的域名必须是目标窗口的域名。

好赞!

老师的blog质量确实高!能学到不少东西!

jsonp服务器不用做任何改造说法有些不妥。服务端必须要调整以返回callback(...)

引用fyunli的发言:

jsonp服务器不用做任何改造说法有些不妥。服务端必须要调整以返回callback(...)

恩 是的 所以既然都希望客户端调用了 那还是用CORS吧 真正意义上的不需要修改 只需要多输出个HTTP头

引用fyunli的发言:

jsonp服务器不用做任何改造说法有些不妥。服务端必须要调整以返回callback(...)

谢谢指出,改了一下。

一点也不详细,唉

P3P 的介绍呢

你好,咨询个问题:
wp的函数:get_the_date(‘Y-m-d g:i:s +08:00’);
怎样输出2015-12-16 T 17:47:53+08:00 这种格式,中间有个“T”符合ISO8601规范的UTC格式

写的很好,对于教学来说已经足够。但,浏览器的同源策略,是黑客攻击的重要点,任何一个实现上的偏差都可能导致被突破,说一些你没提到的吧。

1. cookie 的不区分协议与端口
2. Edge 的 SOP 实现不区分端口(截至回复日期)

给你留个问题:
在具体业务使用 event.origin 如何安全的匹配来自其他业务子域名的 origin?

window.postMessage 那块:
var popup = window.open('http://aaa.com', 'title');
popup.postMessage('Hello World!', 'http://aaa.com');

所有的"http://aaa.com" 不应该是“http://bbb.com”? 这才是targetOrigin。

@Muqi Li:

这里有一个例子,你可以试试看。
https://davidwalsh.name/window-postmessage

这年头连tampermonkey扩展都弹跨域请求提示了,然而那提示做得逼死选择障碍症。

引用阮一峰的发言:

@Muqi Li:

这里有一个例子,你可以试试看。
https://davidwalsh.name/window-postmessage

谢谢,但是既然是aaa向bbb发消息,难道不应该?
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

@Muqi:

是我搞错了,把 aaa 和 bbb 颠倒了,已经改好了。

没有同源策略的风险我觉得说得不够准确。不全是为了防止一个站访问另一个站的cookie,而是在没有同源策略的情况下。当用户在同一个浏览器内,银行和一个恶意网站,在没登出时,恶意网站可以通过脚本请求至银行网站,浏览器自动把银行的登陆cookie带上,从而获取到用户的敏感信息,这个过程恶意站根本不需要直接获取银行站的cookie。由于浏览器的无作为,恶意站的这种操作跟用户自己点击是一样的。维基百科说的较为清晰:

A user visiting that malicious site would expect that the site he is visiting has no access to the banking session cookie. While this is true, the JavaScript has no direct access to the banking session cookie, but it could still send and receive requests to the banking site with the banking site's session cookie, essentially acting as a normal user of the banking site

parent.location.href= target + "#" + hash;
这个不能设置成功吧,跨域的话。子窗口 拿不到父窗口的location.href

你们试过嘛?postMessage明显不生效啊

阮老师,我在chrome下测试,跨域ajax不是不可以发,只是不允许读取请求的响应。
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://xxxxx:xx' is therefore not allowed access.
但是server确实收到响应,并且response是Status Code:200 OK

3.3节中写到:“postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。”

如果第二个参数targetOrigin设为*的话,其含义是不检查目标窗口的origin,与该参数是否匹配。而不是发向所有窗口。

引用大力东的发言:

阮老师,我在chrome下测试,跨域ajax不是不可以发,只是不允许读取请求的响应。
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://xxxxx:xx' is therefore not allowed access.
但是server确实收到响应,并且response是Status Code:200 OK

我也测试了,确实是这样

之前一直这些概念一直迷迷糊糊,现在看完之后醍醐灌顶!阮老师,感谢您指明了我的很多web的疑惑。

"浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。"
阮老师,我刚刚做了一下实验,这句话似乎有点问题...
必须父窗口和子窗口同源,父窗口才能读取到子窗口的name吧?

A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。

这段话描述不准确!

应该是:
A网站网页中设置的 Cookie,在B网站网页不能打开,除非这两个网页"同源"。
即:A与B的 协议,域名,端口号都相同(但路径可以不同),才能互相访问对方的cookies 。

参考维基百科 请给出链接: https://en.wikipedia.org/wiki/Same-origin_policy

阮老师, 有处错误: postMessage 中的e.origin 不是*消息发向的网址*, 而是*发送者的源*.

event.origin属性可以过滤不是发给本窗口的消息。

应该是:
event.origin属性可以判断非法的发送者(或者说不受信的发送者)

Cookie 是服务器写入浏览器的一小段信息,只有“同源”的网页才能共享。

这里有个问题,如果同源的概念是你前面讲的协议、域名、端口都相同那就不对了,我做了两个域名相同的站(协议、端口都不同),如下:

A站:https://www.example.com:3443/

B站:http://www.example.com:3445/

当我登录A站后,生成了A站的cookie,这时B站依然可以通过js获取A站的cookie,与通过 document.domain 设置的原理一样,A站生成cookie时,默认domain是 www.example.com ,所以Cookie共享跟协议、端口无关。

请教一下,在“4.2 WebSocket”中,您谈到

> 正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。

根据[List of HTTP header fields](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#General_format),HTTP协议Request中也有`Origin`字段,
既然HTTP和WebSocket都有`Origin`字段,为什么HTTP协议不能通过它来解决跨域访问的问题呢?
谢谢!

引用hilojack的发言:

阮老师, 有处错误: postMessage 中的e.origin 不是*消息发向的网址*, 而是*发送者的源*.

应该是:
event.origin属性可以判断非法的发送者(或者说不受信的发送者)

我也觉得应该是*发送者的源*

因为浏览器同时还规定,提交表单不受同源政策的限制。 这个对吗?

引用李超的发言:

请教一下,在“4.2 WebSocket”中,您谈到

> 正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。

根据[List of HTTP header fields](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#General_format),HTTP协议Request中也有`Origin`字段,
既然HTTP和WebSocket都有`Origin`字段,为什么HTTP协议不能通过它来解决跨域访问的问题呢?
谢谢!

阮老师你好,我也同样有这个疑惑,经过实测确实不设置CORS, WebSockst也可以跨源通信,但是http协议请求头在跨域访问的时候一样会带上Origin字段,所以说因为有Origin字段不实行同源策略的观点应该不成立吧?

阮老师你好,关于postMessage中的消息监听的回掉函数参数,e.origin 消息发向的网址,这个我感觉有点会让人误解,解释为:消息来源的网址(是一个网址的字符串),可能更好一点。

阮老师您好,在文中「1.3 限制范围」里的第一条——「Cookie、LocalStorage 和 IndexDB 无法读取」中,您想表达的是 IndexedDB 吧?这个位置是否有笔误?

阮老师,不应该是indexedDB吗?还是indexDB和indexedDB是相同的呢?

这一段,浏览器允许通过设置document.domain共享 Cookie。
按照阮前辈的方式cookie在B站读不到,俩页面确实设置了同样的domain,不知前辈是否实践过?

您好,请问window.postMessage部分,父窗口向子窗口发消息是否应该是:

window.opener.postMessage('Nice to see you', 'http://bbb.com');

而子窗口向父窗口发消息则应该是:

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://aaa.com');

引用siren的发言:

您好,请问window.postMessage部分,父窗口向子窗口发消息是否应该是:

window.opener.postMessage('Nice to see you', 'http://bbb.com');

而子窗口向父窗口发消息则应该是:

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://aaa.com');

你正好理解反了

这是子向父发送:window.opener.postMessage('Nice to see you', '父域');

引用源稚竹的发言:

"浏览器窗口有window.name属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它。"
阮老师,我刚刚做了一下实验,这句话似乎有点问题...
必须父窗口和子窗口同源,父窗口才能读取到子窗口的name吧?

是同一窗口,不存在父子关系,当前窗口域名为A时设置widow.name=a,那么不管这个窗口打开什么域名,它的名字都还是a,除非被后来打开的网页更改了

@源稚竹:

之前看到ifram跨域中,利用在不同源子窗口中再嵌入一个同源的子子窗口做代理,以供父窗口读取window.name属性。

js跳转后的页面可以展现初始数据,但不能进行查询操作,报错:Uncaught DOMException:Blocked a frame

是这样,在js里跳转的话,可以跳转页面,也可以看见初始查询出来的数据(估计是页面的$(function(){})这个
里面加载出来的数据),但是做进一步的操作,如通过id查询内容则无反应,报错:Uncaught DOMException :

blocked a frame with origin(“这里是跳转的地址,地址对应的是另一个公司开发的项目,肯定是不同域

的”)... 但是我这边还是想通过js解决这个问题,有没有什么好办法。

我这边不想通过后台跳转解决,想通过前端的js解决问题,因为这样可以不用重启项目,更不用编译什么的,要不然我还得重新搭载环境,我这边也不能要求所跳转的项目有什么更改,是另一个公司做的。

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接