浏览器的同源策略

什么是同源策略

首先来看一个比较官方的定义(浏览器的同源策略 ):同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

这是一个安全机制,假设我们没有这个安全机制会发生什么?

没有同源策略的三个危险场景

这里只展示三个知道的场景,应该没有其它场景除非浏览器有所改变,如有其它场景可在文末补充。

没有同源策略限制的接口请求

设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?

很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,”同源政策”是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了

有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:

  1. 你准备去清空你的购物车,于是打开了买买买网站 www.maimaimai.com ,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
  2. 你在看有什么东西买的过程中,你的好基友发给你一个链接 www.nidongde.com ,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
  3. 你饶有兴致地浏览着 www.nidongde.com ,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向 www.maimaimai.com 发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!如果这不是一个买买买账号,而是你的银行账号,那……
    这就是传说中的CSRF攻击 浅谈CSRF攻击方式
    看了这波CSRF攻击我在想,即使有了同源策略限制,但cookie是明文的,还不是一样能拿下来。于是我看了一些cookie相关的文章聊一聊 cookieCookie/Session的机制与安全,知道了服务端可以设置httpOnly,使得前端无法操作cookie,如果没有这样的设置,像XSS攻击就可以去获取到cookie Web安全测试之XSS;设置secure,则保证在https的加密通信中传输以防截获。

没有同源策略限制的Dom查询

  1. 有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进 www.yinghang.com 改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
  2. 睡眼朦胧的你没看清楚,平时访问的银行网站是 www.yinhang.com ,而现在访问的是 www.yinghang.com ,这个钓鱼网站做了什么呢?
1
<iframe name="yinhang" src="www.yinhang.com"></iframe>
1
2
3
4
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

由此我们知道,同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。

没有同源策略的数据存储

数据存储中的数据虽然没有cookie那么重要,但是也是用户的个人,里面会记录用户的个人行为;假如个人行为泄露会影响用户的生活,有骚扰短信、营销电话、推荐邮件等方式

有同源策略的情况

有了同源策略上面的三个问题就可以避免掉(有些情况还是不能避免的),而且遵守同源策略的规则是可以适当做到跨域的。

同源的定义

1995年,同源策略由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个策略。

如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源

下表给出了相对http://store.company.com/dir/page.html同源检测的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 成功 只有路径不同
http://store.company.com/dir/inner/another.html 成功 只有路径不同
https://store.company.com/secure.html 失败 不同协议 ( https和http )
http://store.company.com:81/dir/etc.html 失败 不同端口 ( http:// 80是默认的)
http://news.company.com/dir/other.html 失败 不同域名 ( news和store )

另请参见文件的源定义: URLs

源的更改

比较官方的说法:脚本可以将 document.domain 的值设置为其当前域或其当前域的父域。如果将其设置为其当前域的父域,则这个较短的父域将用于后续源检查。

假设 http://store.company.com/dir/other.html 文档中的一个脚本执行以下语句:

1
document.domain = "company.com";

这条语句执行之后,页面将会成功地通过对 http://company.com/dir/page.html 的同源检测(假设http://company.com/dir/page.html 将其 document.domain 设置为“company.com”,以表明它希望允许这样做 )。然而,company.com 不能设置 document.domainothercompany.com,因为它不是 company.com 的父域。

端口号是由浏览器另行检查的。任何对document.domain的赋值操作,包括 document.domain = document.domain 都会导致端口号被重写为 null 。因此 company.com:8080 不能仅通过设置 document.domain = "company.com" 来与company.com 通信。必须在他们双方中都进行赋值,以确保端口号都为 null

使用 document.domain 来允许子域安全访问其父域时,您需要在父域和子域中设置 document.domain 为相同的值。不这样做可能会导致权限错误。

跨源访问

根据上面说的三种异常情况主要分为以下三种跨源访问:

  1. 跨源网络访问
    同源策略控制了不同源之间的交互,例如在使用XMLHttpRequest会受到同源策略的约束。
  2. 跨源脚本API访问
    Javascript的APIs中,如 iframe.contentWindow, window.parent, window.openwindow.opener 允许文档间直接相互引用。当两个文档的源不同时,这些引用方式将对 WindowLocation对象的访问添加限制
  3. 跨源数据存储访问
    存储在浏览器中的数据,如StorageIndexedDB、Cookie等。

跨源网络访问

同源策略控制了不同源之间的交互。例如在使用XMLHttpRequest会受到同源策略的约束,可使用一些方法允许跨域的方法。同时还有允许跨源的元素。而且跨源网络访问是跨源里面的常见问题。 这些交互通常分为三类:

  • 通常允许跨域写操作(Cross-origin writes)。例如链接(links但在使用其中的属性download时需要同源),重定向以及表单提交。特定少数的HTTP请求需要添加 preflight
  • 通常允许跨域资源嵌入(Cross-origin embedding)。之后下面会举例说明。
  • 通常不允许跨域读操作(Cross-origin reads)。但常可以通过内嵌资源来巧妙的进行读取访问。

允许跨域资源嵌入的元素与示例

  • <script src="..."></script> 标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。
  • <link rel="stylesheet" href="..."> 标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type 消息头。
  • <img>嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,…
  • <video><audio>嵌入多媒体资源。
  • <object>, <embed><applet> 的插件。
  • @font-face 引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。
  • <frame><iframe> 载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。

如何允许跨源访问

当前网络环境下不允许跨源网络访问的只有XMLHttpRequest,因为 ajax 使用的就是 XMLHttpRequest,因此单独开辟一篇文章介绍ajax 跨域

如何阻止跨源访问

  • 阻止跨域写操作,只要检测请求中的一个不可测的标记(CSRF token)即可,这个标记被称为Cross-Site Request Forgery (CSRF) 标记。必须使用这个标记来阻止页面的跨站读操作。
  • 阻止资源的跨站读取,需要保证该资源是不可嵌入的。阻止嵌入行为是必须的,因为嵌入资源通常向其暴露信息。
  • 阻止跨站嵌入,需要确保你的资源不能是以上列出的可嵌入资源格式。多数情况下浏览器都不会遵守 Conten-Type 消息头。例如,如果您在HTML文档中指定 <script\> 标记,则浏览器将尝试将HTML解析为JavaScript。 当您的资源不是您网站的入口点时,您还可以使用CSRF令牌来防止嵌入
  1. X-Frame-Options阻止iframe的嵌套
    可以在nginx中设置nginx配置X-Frame-Options响应头

  2. 这里引申出另外一个问题,防盗链。
    参考防盗链

  3. CSRF
    参考浅谈CSRF

canvas操作图片的跨域问题

解决canvas图片getImageData,toDataURL跨域问题

跨源脚本API访问

Javascript的APIs中,如 iframe.contentWindow, window.parent, window.openwindow.opener 允许文档间直接相互引用。当两个文档的源不同时,这些引用方式将对 WindowLocation对象的访问添加限制。

允许以下对 Window 属性的跨源访问

允许以下对 Location 属性的跨源访问

如果两个网页不同源,就无法拿到对方的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也会报错。

可通过以下几个方法完成跨源

使用 window.postMessage 跨源

这个方法为什么放在这里,是因为该方法是最应该使用的方法,后面的方法都是小道而。

window.postMessage() 是HTML5的一个接口,专注实现不同窗口不同页面的跨域通讯。

这里是http://localhost:9099/#/crossDomain,发消息方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<button @click="postMessage">给http://crossDomain.com:9099发消息</button>
<iframe name="crossDomainIframe" src="http://crossdomain.com:9099"></iframe>
</div>
</template>

<script>
export default {
mounted () {
window.addEventListener('message', (e) => {
// 这里一定要对来源做校验
if (e.origin === 'http://crossdomain.com:9099') {
// 来自http://crossdomain.com:9099的结果回复
console.log(e.data)
}
})
},
methods: {
// 向http://crossdomain.com:9099发消息
postMessage () {
const iframe = window.frames['crossDomainIframe']
iframe.postMessage('我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom', 'http://crossdomain.com:9099')
}
}
}
</script>

这里是http://crossdomain.com:9099,接收消息方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
我是http://crossdomain.com:9099
</div>
</template>

<script>
export default {
mounted () {
window.addEventListener('message', (e) => {
// 这里一定要对来源做校验
if (e.origin === 'http://localhost:9099') {
// http://localhost:9099发来的信息
console.log(e.data)
// e.source可以是回信的对象,其实就是http://localhost:9099窗口对象(window)的引用
// e.origin可以作为targetOrigin
e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这就是你想知道的结果:${document.getElementById('app') ? '有id为app的Dom' : '没有id为app的Dom'}`, e.origin);
}
})
}
}
</script>

结果可以看到

0003

使用 document.domain 跨源

该方法的使用过程在源的更改一节已经讲过,这个方法是有限制的,只能是子域名设置为父域名

使用 URL 锚点标记

URL锚点标记是URL的#号及其后面的部分,#之后的部分(也称为片段标识符)

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

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

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

1
2
3
4
window.onhashchange = function checkMessage() {
var message = window.location.hash;
// ...
}

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

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

父窗口也同样通过监听hashchange事件得到通知。

使用 window.name 属性

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

跨源数据存储访问

存储在浏览器中的数据,如StorageIndexedDB、以源进行分割。每个源都拥有自己单独的存储空间,一个源中的Javascript脚本不能对属于其它源的数据进行读写操作。

  • 这两个可以在iframe中根据情况使用跨源API访问的四种方法取Storage中的值,还是推荐使用postMessage方式。

Cookies 使用不同的源定义方式。一个页面可以为本域和任何父域设置cookie,只要是父域不是公共后缀(public suffix)即可(举例说明下:如果页面域名为 www.baidu.com, domain可以设置为“ www.baidu.com”,也可以设置为“baidu.com”,但不能设置为“.com”或“com”)。Firefox 和 Chrome 使用 Public Suffix List 决定一个域是否是一个公共后缀(public suffix)。Internet Explorer使用其自己的内部方法来确定域是否是公共后缀。不管使用哪个协议(HTTP/HTTPS)或端口号,浏览器都允许给定的域以及其任何子域名(sub-domains) 访问 cookie。设置 cookie 时,你可以使用Domain,Path,Secure,和Http-Only标记来限定其访问性。

  • cookie跨域的方式如果只是简单的跨域可以设置cookie的domain为一级域名,所有的子域名都可以使用。
  • 如果是复杂的跨域,只能是在iframe中根据情况使用跨源API访问的postMessage和锚点标记。
  • 不使用document.domain是因为可以通过一级域名的方式,如果感觉使用这种方式比较开心,也可以使用。

总结

  • 允许跨源访问的元素:script、link、img、video、audio、object、embed、applet、@font-face、frame、iframe。
  • ajax跨域的解决方法:可跨源的元素、JSONP、CORS、Nginx、WebSocket。
  • 如何跨源脚本API访问:postMessage、document.domain、片段标识符、window.name。
  • 如何跨源数据存储访问:postMessage、document.domain、片段标识符、window.name、设置父域cookie(cookie专用)

参考

浏览器的同源策略
档案同源策略
Cross-Origin Resource Sharing (CORS)
不要再问我跨域的问题了
浏览器同源政策及其规避方法
跨域资源共享 CORS 详解
Identifying resources on the Web