查看原文
其他

【第2235期】CORS完全手冊之跨来源的安全性问题

huli 前端早读课 2021-04-09

前言

CORS系列第五篇。今日前端早读课文章由@huli授权分享。

正文从这开始~~

在前面几篇里面,我们知道CORS protocol 基本上就是为了安全性所产生的协定,而除了CORS 以外,其实还有一系列跟跨来源有关的东西,例如说:

  • CORB(Cross-Origin Read Blocking)

  • CORP(Cross-Origin Resource Policy)

  • COEP(Cross-Origin-Embedder-Policy)

  • COOP(Cross-Origin-Opener-Policy)

是不是光看到这一系列很类似的名词就已经头昏眼花了?对,我也是。在整理这些资料的过程中,发现跨来源相关的安全性问题比我想像中还来得复杂,不过花点时间整理之后发现还是有脉络可循,因此这篇会以我觉得应该比较好理解的脉络,去讲解为什么会有这些东西出现。

除了上面这些COXX 的各种东西,还有其他我想提的跨来源相关安全性问题,也会在这篇一并提到。

在继续下去之前先提醒一下大家,这篇在讲的是「跨来源的安全性问题」,而不单单只是「CORS 的安全性问题」。CORS protocol 所保护的东西跟内容在之前都介绍过了,这篇要谈的其实已经有点偏离大标题「CORS」完全手册,因为这跟CORS 协定关系不大,而是把层次再往上拉高,谈谈「跨来源」这件事情。

所以在看底下的东西的时候,不要把它跟CORS 搞混了。除了待会要讲的第一个东西,其他的跟CORS 关系都不大。

CORS misconfiguration

如果你还记得的话,前面我有提到过如果跨来源请求想要带上cookie,那Access-Control-Allow-Origin就不能是*,而是必须指定单一的origin,否则浏览器就不会给过。

但现实的状况是,我们不可能只有一个origin。我们可能有许多的origin,例如说buy.example.com、social.example.com、note.example.com,都需要去存取,这时候我们就没办法写死response header里的origin,而是必须动态调整。

先讲一种最糟糕的写法,就是这样:

  1. app.use( ( req, res, next ) => {

  2. res.headers[ 'Access-Control-Allow-Credentials' ] = 'true'

  3. res.headers[ 'Access-Control-Allow-Origin' ] = req.headers [ 'Origin' ]

  4. })

为了方便起见,所以直接映射request header 里面的origin。这样做的话,其实就代表任何一个origin 都能够通过CORS 检查。

这样做会有什么问题呢?

问题可大了。

假设我今天做一个钓鱼网站,网址是http://fake-example.com,并且试图让使用者去点击这个网站,而钓鱼网站里面写了一段script:

  1. //用api去拿使用者资料,并且带上cookie

  2. fetch( 'http://api.example.com/me' , {

  3. credentials: 'include'

  4. })

  5. .then( res => res.text())

  6. .then( res => {

  7. //成功拿到使用者资料,我可以传送到我自己的server

  8. console .log(res)


  9. //把使用者导回真正的网站

  10. window .location = ' http :// example . com '

  11. })

我用fetch去http://api.example.com/me拿资料,并且带上cookie。接着因为server不管怎样都会回覆正确的header,所以CORS检查就通过了,我就拿到资料了。

因此这个攻击只要使用者点了钓鱼网站并且在是登入状态,example.com就会中招。至于影响范围要看网站的api,最基本的就是只拿得到使用者资料,比较严重一点的可能可以拿到user token(如果有这个api)。

这个攻击有几件事情要注意:

  • 这不是XSS,因为我没有在example.com执行程式码,我是在我自己的钓鱼网站http://fake-example.com上执行

  • 这有点像是CSRF,但是网站通常对于GET 的API 并不会加上CSRF token 的防护,所以可以过关

  • 如果有设定SameSite cookie,攻击就会失效,因为cookie 会带不上去

因此这个攻击要成立有几个前提:

  • CORS header 给到不该给的origin

  • 网站采用cookie 进行身份验证,而且没有设定SameSite

  • 使用者要主动点击钓鱼网站并且是登入状态

针对第一点,可能没有人会像我上面那样子写,直接用request header 的origin。比较有可能的做法是这样:

  1. app.use( ( req, res, next ) => {

  2. res.headers[ 'Access-Control-Allow-Credentials' ] = 'true'

  3. const origin = req.headers[ 'Origin' ]


  4. //侦测是不是example.com结尾

  5. if ( /example\.com$/.test(origin)) {

  6. res.headers[ 'Access-Control-Allow-Origin' ] = origin

  7. }

  8. })

如此一来,底下的origin 都可以过关:

  • example.com

  • buy.example.com

  • social.example.com

可是这样写是有问题的,因为这样也可以过关:fakeexample.com

像是这类型的漏洞是经由错误的CORS 设置引起,所以称为CORS misconfiguration。

而解决方法就是不要用RegExp 去判断,而是事先准备好一个清单,有在清单中出现的才通过,否则都是失败,如此一来就可以保证不会有判断上的漏洞,然后也记得把cookie 加上SameSite 属性。

  1. const allowOrigins = [ 'example.com', 'buy.example.com','social.example. com' ]


  2. app.use( ( req, res, next ) => {

  3. res.headers[ 'Access-Control-Allow -Credentials' ] = 'true'

  4. const origin = req.headers[ 'Origin' ]


  5. if (allowOrigins.includes(origin)) {

  6. res.headers[ 'Access-Control-Allow-Origin' ] = origin

  7. }

  8. })

想知道更多的话可以参考:

  • 3 Ways to Exploit Misconfigured Cross-Origin Resource Sharing (CORS)

  • JetBrains IDE Remote Code Execution and Local File Disclosure

  • AppSec EU 2017 Exploiting CORS Misconfigurations For Bitcoins And Bounties by James Kettle

绕过Same-origin Policy?

除了CORS以外,Same-origin policy其实出现在浏览器的各个地方,例如说window.open以及iframe。当你使用window.open打开一个网页的时候,回传值会是那个新的网页的window(更精确来说是WindowProxy啦,可以参考MDN: Window.open()),但只有在same origin的状况下才能存取,如果不是same origin的话,只能存取很小一部分的东西。

假设我现在在好了,然后写了这一段script:a.example.com

  1. var win = window.open( 'http://b.example.com' )

  2. //等新的页面载入完成

  3. setTimeout( () => {

  4. console .log(win)

  5. }, 2000 )

用window.open去开启b.example.com,等页面载入完成之后去存取的window。

执行之后会看到console 有一段错误:

因为a.example.com跟b.example.com是cross origin,所以没办法存取到window。这个规范其实也十分合理,因为如果能存取到window的话其实可以做蛮多事情的,所以限制在same origin底下才能拿到window。

不过「没办法存取window」这个说法不太精确,因为就算是cross origin,仍然有一些操作是允许的,例如说:

  1. var win = window.open( 'http://b.example.com')

  2. //等新的页面载入完成

  3. setTimeout( () => {

  4. //变更开启的window的位置

  5. win.location = 'https://google.com'

  6. setTimeout( () => {

  7. //关闭视窗

  8. win.close()

  9. }, 2000 ) },

  10. 2000 )

可以改变开启的window的location,也可以关闭开启的视窗。

相对地,身为被开启的那个视窗(b.example.com),也可以用window.opener拿到开启它的网页(a.example.com)的window,不过一样只有部分操作是被允许的。

但是呢,如果这两个网站是在同一个subdomain底下,而且你对两个网站都有控制权,是可以透过更改document.domain来让他们的origin相同的!

在a.example.com,这样子做:

  1. //新增这个,把domain设为example.com

  2. document .domain = 'example.com'


  3. var win = window .open( 'http://b.example.com' )

  4. //等新的页面载入完成

  5. setTimeout( () => {

  6. console .log(win.secret)

  7. // 12345

  8. }, 2000 )

在b.example.com里面也需要做一样的事情:

  1. document.domain = 'example.com '

  2. window .secret = 12345

然后你就会神奇地发现,你现在可以拿到的window了!而且几乎是什么操作都可以做。

更详细的介绍可以参考MDN:Document.domain,会这样可能是有什么历史因素,但未来因为安全性的问题有可能会被拔掉就是了。

相关的spec可以参考:7.5.2 Relaxing the same-origin restriction

进入正题:其他各种COXX 是什么?

前面这两个其实都只是小菜而已,并不是这一篇着重的主题。这一篇最想跟大家分享的其实是:

  • CORB(Cross-Origin Read Blocking)

  • CORP(Cross-Origin Resource Policy)

  • COEP(Cross-Origin-Embedder-Policy)

  • COOP(Cross-Origin-Opener-Policy)

这几个东西。

开头我有提过了,这几个东西没有好好讲的话很容易搞混,所以我会用我自己觉得可能比较好懂的方式来讲解,接下来就开始吧。

严重的安全漏洞:Meltdown 与Spectre

在2018年1月3号,Google的Project Zeror对外发布了一篇名为:Reading privileged memory with a side-channel的文章,里面讲述了三种针对CPU data cache的攻击:

  • Variant 1: bounds check bypass (CVE-2017-5753)

  • Variant 2: branch target injection (CVE-2017-5715)

  • Variant 3: rogue data cache load (CVE-2017-5754)

而前两种又被称为Spectre,第三种被称为是Meltdown。如果你有印象的话,在当时这可是一件大事,因为问题是出在CPU,而且并不是个容易修复的问题。

而这个漏洞的公布我觉得对于浏览器的运作机制有满大的影响(或至少加速了浏览器演进的过程),尤其是spectre 可以拿来攻击浏览器,而这当然也影响了这系列的主题:跨来源资源存取。

因此,稍微理解一下Spectre 在干嘛我觉得是很有必要的。如果想要完全理解这个攻击,需要有满多的背景知识,但这不是这一篇主要想讲的东西,因此底下我会以非常简化的模型来解释Spectre,想要完全理解的话可以参考上面的连结。

超级简化版的Spectre 攻击解释

再次强调,这是为了方便理解所简化过的版本,跟原始的攻击有一定出入,但核心概念应该是类似的。

假设现在有一段程式码(C 语言)长这样子:

  1. uint8_t arr1[ 16 ] = { 1 , 2 , 3 };

  2. uint8_t arr2[ 256 ];

  3. unsigned int array1_size = 16 ;


  4. void run ( size_t x) { if (x < array1_size) { uint8_t y = array2[array1[x]]; }}


  5. size_t x = 1 ;

  6. run(x);

我宣告了两个阵列,型态是uint8_t,所以每个阵列的元素大小都会是1 个byte(8 bit)。而arr1 的长度是16,arr2 的长度是256。

接下来我有一个function叫做run,会吃一个数字x,然后判断x是不是比array1_size小,是的话我就先把array1[x]的值取出来,然后作为索引去存取array2,再把拿到的值给y 。

以上面的例子来说,run(1)的话,就会执行:

  1. uint8_t y = array2[array1[ 1 ]];

而array1[1]的值是2,所以就是y = array2[2]。

这段程式码看起来没什么问题,而且我有做了阵列长度的判断,所以不会有超出阵列索引(Out-of-Bounds,简称OOB)的状况发生,只有在x 比array1_size 小的时候才会继续往下执行。

不过这只是你看起来而已。

在CPU 执行程式码的时候,有一个机制叫做branch prediction。为了增进程式码执行的效率,所以CPU 在执行的时候如果碰到if 条件,会先预测结果是true 还是false,如果预测的结果是true,就会先帮你执行if 里面的程式码,把结果先算出来。

刚刚讲的都只是「预测」,等到实际的if 条件执行完之后,如果跟预测的结果相同,那就皆大欢喜,如果不同的话,就会把刚刚算完的结果丢掉,这个机制称为:预测执行(speculatively execute)

因为CPU 会把结果丢掉,所以我们也拿不到预测执行的结果,除非CPU 有留下一些线索。

而这就是Spectre 攻击成立的主因,因为还真的有留下线索。

一样是为了增进执行的效率,在预测执行的时候会把一些结果放到CPU cache 里面,增进之后读取资料的效率。

假设现在有ABC 三个东西,一个在CPU cache 里面,其他两个都不在,我们要怎么知道是哪一个在?

答案是,透过存取这三个东西的时间!因为在CPU cache 里面的东西读取一定比较快,所以如果读取A 花了10ms,B 花了10ms,C 只花了1ms,我们就知道C 一定是在CPU cache 里面。这种透过其他线索来得知资讯的攻击方法,叫做side-channel attack,从其他管道来得知资讯。

上面的方法我们透过时间来判断,所以又叫做timing-attack。

结合上述知识之后,我们再回来看之前那段程式码:

  1. uint8_t arr1[ 16 ] = { 1 , 2 , 3 };

  2. uint8_t arr2[ 256 ];

  3. unsigned int array1_size = 16 ;


  4. void run ( size_t x) { if (x < array1_size) { uint8_t y = array2[array1[x]]; }}



  5. size_t x = 1 ;

  6. run(x);

假设现在我跑很多次run(10),CPU根据branch prediction的机制,合理推测我下一次也会满足if条件,执行到里面的程式码。就在这时候我突然把x设成100,跑了一个run(100)。

这时候if 里面的程式码会被预测执行:

  1. uint8_t y = array2[array1[ 100 ]];

假设array1[100]的值是38好了,那就是y = array2[38],所以array2[38]会被放到CPU cache里面,增进之后载入的效率。

接着实际执行到if condition 发现条件不符合,所以把刚刚拿到的结果丢掉,什么事都没发生,function 执行完毕。

然后我们根据刚刚上面讲的timing attack,去读取array2的每一个元素,并且计算时间,会发现array2[38]的读取时间最短。

这时候我们就知道了一件事:array1[100] 的内容是38

你可能会问说:「那你知道这能干嘛?」,能做的事情可多了。array1 的长度只有16,所以我读取到的值并不是array1 本身的东西,而是其他部分的记忆体,是我不应该存取到的地方。而我只要一直复制这个模式,就能把其他地方的资料全都读出来。

这个攻击如果放在浏览器上面,我就能读取同一个process 的其他资料,换句话说,如果同一个process 里面有其他网站的内容,我就能读取到那个网站的内容!

这就是Spectre 攻击,透过CPU 的一些机制来进行side-channal attack,进而读取到本来不该读到的资料,造成安全性问题。

所以用一句白话文解释,在浏览器上面,Spectre 可以让你有机会读取到其他网站的资料。

有关Spectre 的解释就到这里了,上面简化了很多细节,而那些细节我其实也没有完全理解,想知道更多的话可以参考:

  • Reading privileged memory with a side-channel

  • 解读Meltdown & Spectre CPU 漏洞

  • 浅谈处理器级Spectre Attack及Poc分析

  • [闲聊] Spectre & Meltdown漏洞概论(翻译)

  • Spectre漏洞示例代码注释

  • Google update: Meltdown/Spectre

  • Mitigating Spectre with Site Isolation in Chrome

而那些COXX 的东西,目的都是差不多的,都是要防止一个网站能够读取到其他网站的资料。只要不让恶意网站跟目标网站处在同一个process,这类型的攻击就失效了。

从这个角度出发,我们来看看各种相关机制。

CORB(Cross-Origin Read Blocking)

Google于Spectre攻击公开的一个月后,也就是2018年2月,在部落格上面发了一篇文章讲述Chrome做了哪些事情来防堵这类型的攻击:Meltdown/Spectre。

文章中的Cross-Site Document Blocking就是CORB的前身。根据Chrome Platform Status,在Chrome for desktop release 67的时候正式预设启用,那时候大概是2018年5月,也差不多那个时候,被merge进去fetch的spec,成为规格的一部分(CORB: blocking of nosniff and 206 responses)。

前面有提到过Spectre 能够读取到同一个process 底下的资料,所以防御的其中一个方式就是不要让其他网站的资料出现在同一个process 底下。

一个网站有许多方式可以把跨来源的资源设法弄进来,例如说fetch或是xhr,但这两种已经被CORS给控管住了,而且拿到的response应该是存在network相关的process而不是网站本身的process,所以就算用Spectre也读不到。

但是呢,用 <img>或是 <script>这些标签也可以轻易地把其他网站的资源载入。例如说: <imgsrc="https://bank.com/secret.json">,假设是个机密的资料,我们就可以把这个机密的资料给「载入」。

你可能会好奇说:「这样做有什么用?那又不是一张图片,而且我用JS 也读取不到」。没错,这不是一张图片,但以Chrome 的运作机制来说,Chrome 在下载之前不知道它不是图片(有可能副档名是.json 但其实是图片对吧),因此会先下载,下载之后把结果丢进render process,这时候才会知道这不是一张图片,然后引发载入错误。

看起来没什么问题,但别忘了Spectre 开启了一扇新的窗,那就是「只要在同一个process 的资料我都有机会读取到」。因此光是「把结果丢进render process」这件事情都不行,因为透过Spectre 攻击,攻击者还是拿得到存在记忆体里面的资料。

因此CORB 这个机制的目的就是:

如果你想读的资料类型根本不合理,那我根本不需要把读到render process,我直接把结果丢掉就好!

延续上面的例子,那个json 档的MIME type 如果是application/json,代表它绝对不会是一张图片,因此也不可能放到img 标签里面,这就是我所说的「读的资料类型不合理」。

CORB 主要保护的资料类型有三种:HTML、XML 跟JSON,那浏览器要怎么知道是这三种类型呢?不如就从response header 的content type 判断吧?

很遗憾,没办法。原因是有很多网站的content type是设定错误的,有可能明明就是JavaScript档案却设成text/html,就会被CORB挡住,网站就会坏掉。

因此Chrome会根据内容来探测(sniffing)档案类型是什么,再决定要不要套用CORB。

但这其实也有误判的可能,所以如果你的伺服器给的content type都确定是正确的,可以传一个response header是X-Content-Type-Options: nosniff,Chrome就会直接用你给的content type而不是自己探测。

总结一下,CORB是个已经预设在Chrome里的机制,会自动阻挡不合理的跨来源资源载入,像是用 <img>来载入json或是用 <script>载入HTML等等。而除了Chrome之外,Safari跟Firefox好像都还没实装这个机制。

更详细的解释可以参考:

  • Cross-Origin Read Blocking for Web Developers

  • Cross-Origin Read Blocking (CORB)

CORP(Cross-Origin Resource Policy)

CORB 是浏览器内建的机制,自动保护了HTML、XML 与JSON,不让他们被载入到跨来源的render process 里面,就不会被Spectre 攻击。但是其他资源呢?如果其他类型的资源,例如说有些照片跟影片可能也是机密资料,我可以保护他们吗?

这就是CORP这个HTTP response header的功能。CORP的前身叫做From-Origin,下面引用一段来自Cross-Origin-Resource-Policy (was: From-Origin) #687的叙述:

Cross-Origin Read Blocking (CORB) automatically protects against Spectre attacks that load cross-origin, cross-type HTML, XML, and JSON resources, and is based on the browser's ability to distinguish resource types. We think CORB is a good idea. From-Origin would offer servers an opt-in protection beyond CORB.

如果你自己知道该保护哪些资源,那就可以用CORP 这个header,指定这些资源只能被哪些来源载入。CORP 的内容有三种:

  • Cross-Origin-Resource-Policy: same-site

  • Cross-Origin-Resource-Policy: same-origin

  • Cross-Origin-Resource-Policy: cross-origin

第三种的话就跟没有设定是差不多的(但其实跟没设还是有差,之后会解释),就是所有的跨来源都可以载入资源。接下来我们实际来看看设定这个之后会怎样吧!

我们先用express起一个简单的server,加上CORP的header然后放一张图片,图片网址是:http://b.example.com/logo.jpg

  1. app.use( ( req, res, next ) => {

  2. res.header( 'Cross-Origin-Resource-Policy' , 'same-origin' )

  3. next()

  4. })

  5. app.use(express.static( 'public' ));

接着在引入这张图片:http://a.example.com

  1. < img src = "http://b.example.com/logo.jpg" />

重新整理打开console,就会看到图片无法载入的错误讯息,打开network tab 还会跟你详细解释原因:

如果把header改成same-site或是cross-origin,就可以看到图片正确被载入。

所以这个header其实就是「资源版的CORS」,原本的CORS比较像是API或是「资料」间存取的协议,让跨来源存取资料需要许可。而资源的载入例如说使用 <img>或是 <script>,想要阻止跨来源载入的话,应该是只能透过server side自行去判断Origin或是Referer之类的值,动态决定是否回传资料。

而CORP 这个header 出现之后,提供了阻止「任何跨来源载入」的方法,只要设定一个header 就行了。所以这不只是安全性的考量而已,安全性只是其中一点,重点是你可以阻止别人载入你的资源。

就如同CORP的前身From-Origin的spec所写到的:

The Web platform has no limitations on embedding resources from different origins currently. Eg an HTML document on http://example.org can embed an image from http://corp.invalid without issue. This has led to a number of problems:

对于这种embedded resource,基本上Web 没有任何限制,想载入什么就载入什么,虽然方便但也会造成一些问题,像是:

Inline linking — the practice of embedding resources (eg images or fonts) from another server, causing the owner of that server to get a higher hosting bill.

Clickjacking — embedding a resource from another origin and attempting to let the visitor click on a concealed link thereof, causing harm to the visitor.

例如说在我的部落格直接连到别人家的图片,这样流量就是别人家server 的,帐单也是他要付。除此之外也会有Clickjacking 的问题。

Privacy leakage — sometimes resource availability depends on whether a visitor is signed in to a particular website. Eg only with a I'm-signed-in-cookie will an image be returned, and if there is no such cookie an HTML document. An HTML document embedding such a resource (requested with the user's credentials) can figure out the existence of that resource and thus whether the visitor is signed in and therefore has an account with a particular service.

这个我之前有看过一个网站但找不到连结了,他可以得知你在某些网站是不是登入状态。那他怎么知道的呢?因为有些资源可能只有在你登入的时候有权限存取。假设某个图片网址只有登入状态下会正确回传图片,没登入的话就会回传server error,那我只要这样写就好:

  1. <img src="xxx" onerror="alert('not login')" onload="alert('login')" >

透过图片是否载入成功,就知道你是否登入。不过设定了SameSite cookie 之后应该就没这问题了。

License checking — certain font licenses require that the font be prevented from being embedded on other origins.

字型网站会阻止没有license 的使用者载入字型,这种状况也很适合用这个header。

总而言之呢,前面介绍的CORB 只是「阻止不合理的读取」,例如说用img 载入HTML,这纯粹是为了安全性考量而已。

但是CORP 则是可以阻止任何的读取(除了iframe,对iframe 没作用),可以保护你网站的资源不被其他人载入,是功能更强大而且应用更广泛的一个header。

现在主流的浏览器都已经支援这个header 了。

Site Isolation

要防止Spectre 攻击,有两条路线:

  • 不让攻击者有机会执行Spectre 攻击

  • 就算执行攻击,也拿不到想要的资讯

前面有提过Spectre 攻击的原理,透过读取资料的时间差得知哪一个资料被放到cache 里面,就可以从记忆体里面「偷」资料出来。那如果浏览器上面提供的计时器时间故意不精准的话,不就可以防御了吗?因为攻击者算出来的秒数会差不多,根本不知道哪一个读取比较快。

Spectre 攻击出现之后浏览器做了两件事:

  • 降低performance.now的精准度

  • 停用 SharedArrayBuffer

第一点很好理解,降低拿时间函式的精准度,就可以让攻击者无法判断正确的读取速度。那第二点是为什么呢?

先讲一下SharedArrayBuffer这东西好了,这东西可以让你document的JS跟web worker共用同一块记忆体,共享资料。所以在web worker里面你可以做一个counter一直累加,然后在JS里面读取这个counter,就达成了计时器的功能。

所以Spectre 出现之后,浏览器就做了这两个调整,从「防止攻击源头」的角度下手,这是第一条路。

而另一条路则是不让恶意网站拿到跨来源网站的资讯,就是前面所提到的CORB,以及现在要介绍的:Site Isolation。

先来一段来自Site Isolation for web developers的介绍:

Site Isolation is a security feature in Chrome that offers an additional line of defense to make such attacks less likely to succeed. It ensures that pages from different websites are always put into different processes, each running in a sandbox that limits what the process is allowed to do. It also blocks the process from receiving certain types of sensitive data from other sites

简单来说呢,Site Isolation 会确保来自不同网站的资源会放在不同的process,所以就算你在自己的网站执行了Spectre 攻击也没关系,因为你读不到其他网站的资料。

Site Isolation 目前在Chrome 是预设启用的状态,相对应的缺点是使用的记忆体会变多,因为开了更多的process,其他的影响可以参考上面那篇文章。

而除了Site Isolation 之外,还有另外一个很容易搞混的东西(我在写这篇的时候本来以为是一样的,后来才惊觉原来不同),叫做:「cross-origin isolated state」。

这两者的差别在哪里呢?根据我自己的理解(不保证完全正确),在Mitigating Spectre with Site Isolation in Chrome这篇文章中有提到:

Note that Chrome uses a specific definition of “site” that includes just the scheme and registered domain. Thus, https://google.co.uk would be a site, and subdomains like https://maps.google.co.uk would stay in the same process.

Site Isolation的“Site”的定义就跟same site一样,跟是same site,所以尽管在Site Isolation的状况下,这两个网页还是会被放在同一个process里面。http://a.example.comhttp://b.example.com

而cross-origin isolated state应该是一种更强的隔离,只要不是same origin就隔离开来,就算是same site也一样。因此跟是会被隔离开来的。而且Site Isolation隔离的对象是process,cross-origin isolated看起来是隔离browsing context group,不让跨来源的东西处在同一个browsing context group。http://a.example.comhttp://b.example.com

而这个cross-origin isolated state 并不是预设的,你必须在你的网页上设置这两个header 才能启用:

  • Cross-Origin-Embedder-Policy: require-corp

  • Cross-Origin-Opener-Policy: same-origin

至于为什么是这两个,待会告诉你。

COEP(Cross-Origin-Embedder-Policy)

要达成cross-origin isolated state 的话,必须保证你对于自己网站上所有的跨来源存取,都是合法的并且有权限的。

COEP(Cross-Origin-Embedder-Policy)这个header 有两个值:

  • unsafe-none

  • require-corp

第一个是预设值,就是没有任何限制,第二个则是跟我们前面提到的CORP(Cross-Origin-Resource-Policy) 有关,如果用了这个require-corp 的话,就代表告诉浏览器说:「页面上所有我载入的资源,都必须有CORP 这个header 的存在(或是CORS),而且是合法的」

现在假设我们有个网站a.example.com,我们想让它变成cross-rogin isolated state,因此帮它加上一个header:Cross-Origin-Embedder-Policy: require-corp,然后网页里面引入一个资源:

  1. < img src ="http://b.example.com/logo.jpg" >

接着我们在b 那边传送正确的header:

  1. app.use( ( req, res, next ) => {

  2. res.header( 'Cross-Origin-Resource-Policy' , 'cross-origin' )

  3. next()

  4. })

如此一来就达成了第一步。

另外,前面我有讲过CORP没有设跟设定成cross-origin有一个细微的差异,就是差在这边。上面的范例如果b那边没有送这个header,那Embedder Policy就不算通过。

COOP(Cross-Origin-Opener-Policy)

而第二步则是这个COOP(Cross-Origin-Opener-Policy)的header,在上面的时候我有说过当你用window.open开启一个网页的时候,你可以操控那个网页的location;而开启的网页也可以用window.opener来操控你的网页。

而这样子让window 之间有关连,就不符合跨来源的隔离。因此COOP 这个header 就是来规范window 跟opener 之间的关系,一共有三个值:

  • Cross-Origin-Opener-Policy: unsafe-none

  • Cross-Origin-Opener-Policy: same-origin

  • Cross-Origin-Opener-Policy: same-origin-allow-popups

第一个就是预设值,不解释,因为没什么作用。

第二个最严格,如果你设定成same-origin的话,那「被你开启的window」也要有这个header,而且也要设定成same-origin,你们之间才能共享window。

底下我们来做个实验,我们有两个网页:

  • http://localhost:5566/page1.html

  • http://localhost:5566/page2.html

page1.html 的内容如下:

  1. <script>

  2. var win = window.open( 'http://localhost:5566/page2.html' ) setTimeout( () => {

  3. console.log(win.secret)

  4. }, 2000 )

  5. </script>

page2.html 的内容如下:

  1. <script> window.secret = 5566 </script >

测验的方式很简单,如果page1 成功输出5566,代表两个之间有共享window。否之则否。

先来试试看不加任何header 吧!由于这两个是same origin,因此本来就可以共享window,成功印出了5566。

接下来我们把server 端的程式码改成这样:

  1. app.use( ( req, res, next ) => {

  2. if (req.url === '/page1.html' ) {

  3. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin' )

  4. }

  5. next()

  6. })

只有page1.html有COOP,page2.html没有,实验的结果是:「无法共享」。就算改成这样:

  1. app.use( ( req, res, next ) => {

  2. if (req.url === '/page1.html' ) {

  3. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin' )

  4. }

  5. if (req.url === '/page2.html' ) {

  6. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin-allow-popups' )

  7. }

  8. next()

  9. })

也是无法共享,因为same-origin的条件就是:

  • 开启的window 要在同一个origin

  • 开启的window 的response header 要有COOP,而且值一定要是 same-origin

只有符合这两点,才能成功存取到完整的window。而且有一点要特别注意,那就是一旦设定了这个header但是没有符合规则,不只存取不到完整的window,连之前那什么openedWindow.close跟window.opener都会拿不到,两个window之间就是彻彻底底没关联了。

再来same-origin-allow-popups的条件比较宽松,只有:

  • 开启的window 要在同一个origin

  • 开启的window 没有COOP,或是COOP 的值不是same-origin

简单来说,same-origin不只保护他人也保护自己,当你设定成这个值的时候,无论你是open别人的,或是被open的,都一定要是same origin然后有相同的header,才能互相存取window。

举一个例子,我调整成这样:

  1. app.use( ( req, res, next ) => {

  2. if (req.url === '/page1.html' ) {

  3. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin-allow -popups')

  4. }

  5. next()

  6. })

只有page1有设定same-origin-allow-popups,page2什么都没设定,这种状况可以互相存取window。

接下来如果两个一样的话:

  1. app.use( ( req, res, next ) => {

  2. if (req.url === '/page1.html' ) {

  3. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin-allow -popups' )

  4. }

  5. if (req.url === '/page2.html' ) {

  6. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin-allow-popups' )

  7. }

  8. next()

  9. } )

这也可以,没什么问题。

那如果是这样呢?

  1. app.use( ( req, res, next ) => {

  2. if (req.url === '/page1.html' ) {

  3. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin-allow -popups' )

  4. }

  5. if (req.url === '/page2.html' ) {

  6. res.header( 'Cross-Origin-Opener-Policy' , 'same-origin' )

  7. }

  8. next()

  9. })

这样子就不行。

所以稍微总结一下,假设现在有一个网页A 用window.open 开启了一个网页B:

  • 如果AB是cross-origin,浏览器本来就有限制,只能存取window.location或是window.close之类的方法。没办法存取DOM或其他东西

  • 如果AB 是same-origin,那他们可以互相存取几乎完整的window,包括DOM。

  • 如果A加上COOP header,而且值是same-origin,代表针对第二种情况做了更多限制,只有B也有这个header而且值也是same-origin的时候才能互相存取window。

  • 如果A加上COOP header,而且值是same-origin-allow-popups,也是对第二种情况做限制只是比较宽松,只要B的COOP header不是same-origin就可以互相存取window。

总之呢,要「有机会互相存取window」,一定要先是same origin,这点是不会变的。实际上是不是存取的到,就要看有没有设定COOP header以及header的值。而如果有设定COOP header但不符合规则,那window.opener会直接变成null,你连location都拿不到(没设定规则的话,就算是cross origin也拿得到)。

其实根据spec还有第四种:same-origin-plus-COEP,但看起来更复杂就先不研究了。

再回到cross-origin isolated state

前面提到了cross-origin isolated state 需要设置这两个header:

  • Cross-Origin-Embedder-Policy: require-corp

  • Cross-Origin-Opener-Policy: same-origin

为什么呢?因为一旦设置了,就代表页面上所有的跨来源资源都是你有权限存取的,如果没有权限的话会出错。所以如果设定而且通过了,就代表跨来源资源也都允许你存取,就不会有安全性的问题。

在网站上可以用:

  1. self.crossOriginIsolated

来判定自己是不是进入cross-origin isolated state。是的话就可以用一些被封印(?)的功能,因为浏览器知道你很安全。

另外,如果进入了这个状态,一开始讲过的透过修改document.domain绕过same-origin policy的招数就不管用了,浏览器就不会让你修改这个东西了。

想知道更多COOP 与COEP 还有cross-origin isolated state,可以参考:

  • Making your website “cross-origin isolated” using COOP and COEP

  • Why you need “cross-origin isolated” for powerful features

  • COEP COOP CORP CORS CORB - CRAP that's a lot of new stuff!

  • Making postMessage() work for SharedArrayBuffer (Cross-Origin-Embedder-Policy) #4175

  • Restricting cross-origin WindowProxy access (Cross-Origin-Opener-Policy) #3740

  • Feature: Cross-Origin Resource Policy

总结

这篇其实讲了不少东西,都是围绕着安全性在打转。一开始我们讲了CORS设定错误会造成的结果以及防御方法,接着讲了透过修改document.cookie让same-site变成same-origin(要两个网站都同意这样做才行),最后则是这篇的重头戏:

  • CORB(Cross-Origin Read Blocking)

  • CORP(Cross-Origin Resource Policy)

  • COEP(Cross-Origin-Embedder-Policy)

  • COOP(Cross-Origin-Opener-Policy)

在找资料的时候花了不少时间,因为名字太像而且功能有些其实有点类似,但看久了其实就会发现差满多的,每一个policy 所注重的地方都不同。希望我整理过后的脉络有帮助大家更好理解这些东西。

如果要用一段话总结这四个东西的话,或许是:

  • CORB:浏览器预设的机制,主要是防止载入不合理的资源,像是用img 载入HTML

  • CORP:是一个HTTP response header,决定这个资源可以被谁载入,可以防止cross-origin 载入图片、影片或任何资源

  • COEP:是一个HTTP response header,确保页面上所有的资源都是合法载入的

  • COOP:是一个HTTP response header,帮same-origin 加上更严格的window 共享设定

相对于其他几篇,我对这篇的内容没有这么熟悉,如果有哪边有讲错麻烦不吝指教,感谢。

关于本文 作者:@huli 原文:https://blog.huli.tw/2021/02/19/cors-guide-5/

为你推荐


【第1639期】如何使用 JSDoc 保证你的 Javascript 类型安全性


【第2027期】图解CORS


欢迎自荐投稿,前端早读课等你来

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存