跨域到底是怎么回事-四个跨域协定
本文最后更新于:1 年前
前面我分别讲解了跨域的基本情况和解决方案,和CORS 到底怎么回事,分别详细介绍了跨域的原因、资源请求限制的问题和方案,也完整、详细地介绍了 CORS 的所有相关内容,以及部分容易出现但难发现的问题方案。
作为本系列的最后一期,将会讲解剩余的跨域安全部分。前面两篇着重讲解的是 CORS,根本原则是不允许 js 脚本获取跨域资源内容,那其实除此之外还有别的协定如下:
- CORB(Cross-Origin-Read-Blocking)
- COOP(Cross-Origin-Opener-Policy)
- CORP(Cross-Origin-Resource-Policy)
- COEP(Cross-Origin-Embedder-Policy)
在介绍这些之前,先提一提这些协定的由来
起因: Spectre
2018 年 1 月, Google Project Zero 对外发布了文章讲述了三种严重的 CPU 漏洞:
- bounds check bypass (CVE-2017-5753)
- branch target injection (CVE-2017-5715)
- rogue data cache load (CVE-2017-5754)
前两种又被称为 Spectre
,第三种被称为是 Meltdown
。Spectre
是一个 2018 年被发现的严重 CPU 漏洞,通过它攻击者可以通过恶意进程获得其他程序在映射内存中的资料内容,具体内容可以看看Spectre (security vulnerability)
简单来说,在浏览器上透过 CPU 的一些机制来进行边信道攻击(side channel attack),获取同一进程内不同网站的内存,或者浏览器本身的内存,特别时在使用一些需要和计算机硬件进行交互的 api 时(performance.measureMemory()、JS Self-Profiling API、SharedArrayBuffer)。虽然 chrome、firefox 也通过站点隔离创建堵截漏洞,但是也因此禁用了部分高风险 api。
因为该漏洞是推测运行机制导致的,需要处理器微码更新堵截,原则上说只要让不同网站运行在不同进程的而下面就能规避该漏洞;而要讲的几种跨域协定,目的就是做好站点隔离(Site Isolation),也是由此产生和切入的安全相关协定。
四个跨域协定
CORB(Cross-Origin-Read-Blocking)
虽然 CORS 已经将跨域资源共享部分限制,但是<link>
、<script>
、<img/>
等标签还是可以将外部资源加载,例如<img src="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf">
,因为浏览器下载前不知道它是否图片资源,所以在下载后会进入渲染进程,这时候就容易被Spectre漏洞
获取。
所以CORB其实是一种算法,当浏览器中跨域加载到达渲染进程前,将可疑、敏感的资源进行识别和阻止,而 CORB 通过响应头的content-type
进行判断就是一项非常好的方案,但是部分服务可能 content-type 是错误的,这时候 Chrome 会根据内容有一套自己的判断法则,基本原理是获取资源的 MIME type 进行嗅探(sniffing),而后再决定是否套用 CORB。
但是 Chrome 也存在误判的可能,即资源的响应头中 content-type 其实才是正确的,而这时候可以在响应头中提供X-Content-Type-Options: nosniff
,强制让 Chrome 不套用自己的嗅探而是直接使用 content-type。
实际表现可以参考这里:
更多内容可以参考:
COOP(Cross-Origin-Opener-Policy)
当你在使用window.open
时,你可以获取打开新窗口的 window 实例,这时候主窗口可以通过这个实例获取该子窗口的所有变量和更改 location,而子窗口也可以通过window.opener
获取主窗口的实例。
这显然违反了站点隔离,所以 COOP 就是用来解决这个问题,这个响应头Cross-Origin-Opener-Policy一共有三个值:
- unsafe-none
- same-origin-allow-popups
- same-origin
- same-origin-allow-popups-plus-coep (目前似乎还在实现,具体可参考这里)
分别解释一下就是,如果主窗口和子窗口是非同源的站点,那就无法互相获取对方,只能使用部分 api 例如 reload、close 等;如果是同源站点,这时候就分以下几种情况,
子窗口 unsafe-none | same-origin-allow-popups | same-origin | |
---|---|---|---|
主窗口 unsafe-none | 允许 | 不允许 | 不允许 |
same-origin-allow-popups | 允许 | 允许 | 不允许 |
same-origin | 不允许 | 不允许 | 允许 |
我们可以准备代码自己做试验:
准备一个 index.html(主窗口):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>主窗口</title> </head> <body> index <script> window.aaa = 'this is index'; document.body.onclick = () => { const win = window.open('http://localhost:9999/index2.html'); console.log('index2 instance:', win); }; </script> </body> </html>
准备一个 index2.html(子窗口):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>子窗口</title> </head> <body> index2 <script> window.aaa = 'this is index2222222'; console.log('index1:', window.opener); </script> </body> </html>
准备一个服务 app.js:
const express = require('express'); const app = express(); app.use((req, res, next) => { if (req.url === '/index.html') { res.header('Cross-Origin-Opener-Policy', 'same-origin'); // 主窗口响应头 } if (req.url === '/index2.html') { res.header('Cross-Origin-Opener-Policy', 'same-origin'); // 子窗口响应头 } next(); }); app.use(express.static('.')); app.listen('9999', () => { console.log('listen in 9999'); });
启动后点击 index 的 body,如果允许共享 window 则会发现:
主窗口:
子窗口:
如果不允许共享 window,则会出现:
主窗口:
子窗口:
当然也有现成的COOP 示例
更多具体详情可以参考这里
COOP 的小替代品: noopener
noopener 属性具有与 COOP 的效果类似的效果,只是它只能从打开程序端工作(当窗口被第三方打开时,无法取消关联)。通过执行 window.open(url, '\_blank', 'noopener')
或 <a target="_blank" rel="noopener">
之类的操作来附加 noopener 时,即可以故意将窗口与打开的窗口断开关联。
虽然 noopener 可以被 COOP 取代,当想在不支持 COOP 的浏览器中保护站点时,它仍然很有用。
CORP(Cross-Origin-Resource-Policy)
这时候已经有 CORS 保护跨域资源共享,COOP 保护跨域页面实例共享,CORP 阻止可疑资源加载,那如果我服务中有图片、音乐等需要保护呢?这时候就是Cross-Origin-Resource-Policy这个响应头负责,这个属性有三个值:
- cross-origin
- same-site
- same-origin
第一个值是允许跨域资源加载,但是与留空不一样,如果页面配置了Cross-Origin-Embedder-Policy: require-corp
,而你的资源没返回Cross-Origin-Resource-Policy: cross-origin
,则也不会被加载(后续会说);same-site 和 same-origin 比较好理解,就是同站(eTLD+1)、同源才允许加载。
可以利用下面代码测试:
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Cross-Origin-Resource-Policy', 'same-origin');
next();
});
app.use(express.static('.'));
app.listen('9999', () => {
console.log('listen in 9999');
});
这时候只要将服务的链接与引用资源的链接不一致时就会出现:
控制台也有提示:
这样就可以保护自己的服务资源不被加载,具体可以参考这里
COEP(Cross-Origin-Embedder-Policy)
这个属性用于告诉浏览器,该站点所有嵌入的资源(img、video、audio、object 等)需要如何校验合法性,它有两个属性:
- unsafe-none
- require-corp
第一个是默认值,即不做任何限制;第二个(require-corp
)意义是:所有的嵌入跨域资源都是需要 corp 这个响应头且合法。
如果站点有响应头Cross-Origin-Embedder-Policy: require-corp
,而嵌入的跨域资源没有Cross-Origin-Resource-Policy: cross-origin
,则会加载失败且报错:
控制台也有提示:
如果你的 COEP 站点需要的跨域资源确实没有设置 CORP,这时候也可以给嵌入资源添加crossorigin
属性,如果该资源响应头带合法Access-Control-Allow-Origin
属性,也可以正常加载渲染:
<img src="http://192.168.88.1:9999/pic.jpg" crossorigin="anonymous" />
cross-origin isolated(跨源隔离)
因为 Spectre 漏洞的原因,针对其漏洞特性,浏览器做了以下动作:
- 部分 api 会被默认禁用(SharedArrayBuffer)
- 减弱了 performance.now() , performance.timeOrigin 的精确性
而如果你真的需要这些 api 或者精确性的话,则首先要让你的站点开启cross-origin isolated(跨源隔离)状态。达到这个状态的条件就是以下两个:
- Cross-Origin-Opener-Policy: same-origin
- Cross-Origin-Embedder-Policy: require-corp
这时候self.crossOriginIsolated属性将会返回true
,表明你的站点已成功隔离。为什么需要这样呢?因为设置了这两项,就表明你的站点跨源加载的资源、打开的窗口都应该是有权限的,都已允许或认证,则表面你的站点是安全的;而且这时候通过之前说的修改document.domain
绕过同源策略的骚操作就不再可用;这时候你的站点已经成功隔离浏览器 context group。
可以参考
总结
总结一下上面说到的四个跨域协定:
- CORB:浏览器的标准协定机制,用于阻止可疑、不合法的资源进入渲染
- COOP:是一个 HTTP headers 的标准,用于帮助严格的 window 实例对象共享规则
- CORP:也是一个 HTTP headers 的标准,可以保护资源不被跨源、跨站的站点加载渲染
- COEP:还是一个 HTTP headers 的标准,确保页面上所有的资源都是合法载入的
最后通过学习前后三篇的关于跨域的文章,你应该深刻了解到了跨域的整个系统知识和方案,·后续也应该对这些问题刀过竹解。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!