聊聊现代浏览器跨域的那点事

大约一年前,WebContainers发布后,我们经常被问到一个问题:为什么它无法在非Chromium内核的浏览器上运行?有人猜测,这是因为我们使用了Chrome独有的应用程序编程接口(API)来支持WebContainers的运行。但事实并非如此。我们也非常希望这项技术能在所有浏览器上顺利运行,可问题远比调整几个设置要复杂得多。 要弄清楚其中的原因,我们得深入研究一下所采用的技术栈。对于WebContainers的正常运转,共享数组缓冲区(SharedArrayBuffer,简称SABs)是极为关键的一部分。共享数组缓冲区在所有浏览器中都存在,只不过默认是不可用的。

共享数组缓冲区的来龙去脉

从底层机制视角来看,WebContainers在很大程度上依赖共享数组缓冲区来实现通信。共享数组缓冲区是一种原始二进制数据缓冲区,能够在多个Web工作线程之间共享。它的一个特性是,借助原子操作(Atomics)API,允许工作线程之间进行同步通信。

2018年初,由于幽灵漏洞(Spectre vulnerability)的出现,所有浏览器供应商都禁用了共享数组缓冲区以及高分辨率定时器。幽灵漏洞使得攻击者有可能读取加载在同一浏览上下文组内的任何数据。

攻击者读取浏览器上下文的数据

浏览上下文是浏览器渲染文档的环境。通常而言,它指的是一个标签页,不过也可能是一个窗口或者内联框架(iframe)。每个浏览上下文都有特定的源,这个源由统一资源定位符(URL)中的协议、域名以及端口来界定。当两个浏览上下文的协议、域名和端口都一致时,它们就属于同一个源。一个浏览上下文组由一组共享相同上下文的浏览上下文构成,比如标签页、窗口或者内联框架。举例来说,如果一个文档在标签页中被渲染,那么该标签页就是浏览上下文。当这个文档打开一个弹出窗口时,弹出窗口拥有自身的浏览上下文,但它们都共享同一个父上下文。由于共享相同的上下文,它们被归为一组,这意味着它们能够通过文档对象模型(DOM)应用程序编程接口(API),比如window.opener,实现相互交互。

攻击者通过测量某些操作的耗时,能够推测出中央处理器(CPU)缓存的内容,进而读取进程内存中的信息。利用低分辨率定时器就可以实施计时攻击,而高分辨率定时器会加快攻击速度。这就是高分辨率定时器因这种漏洞而被禁用的原因。

2020年,一种新的安全方法标准化后,浏览器供应商重新启用了SharedArrayBuffer(共享数组缓冲区)的使用。这些机制十分关键,然而,它们并不能完全防范幽灵漏洞被利用。实际上,这些机制通过防止攻击者从内存的特定部分读取敏感数据,极大地缩小了攻击面。由于共享数组缓冲区默认处于未启用状态,所以若要使用它,必须满足特定条件。

最基础的要求是,文档必须处于安全上下文中。简单来讲,这意味着文档必须通过超文本传输安全协议(HTTPS)来提供服务。如果在内联框架(iframe)中加载文档,若其祖先文档不是通过HTTPS提供服务,那么该文档的上下文就不被视为安全的。

到目前为止,情况还算顺利。如今,大多数网站都通过HTTPS提供服务,所以满足这一要求并不困难。但接下来的要求可就大不一样了。第二个要求是,文档必须是跨域隔离的。下面我们来探讨一下这意味着什么!

跨域隔离

跨域隔离与HTTPS协同工作,让SharedArrayBuffer(共享数组缓冲区)能够正常使用。不仅如此,它还使得performance.measureUserAgentSpecificMemory()(获取用户内存使用情况)可用于测量网页的总内存使用量。由于幽灵漏洞的缘故,高分辨率定时器此前被禁用。有了跨域隔离,performance.now()(性能计时函数)和performance.timeOrigin(性能时间原点)的分辨率从100微秒及以上降低到5微秒及以上,高分辨率定时器也因此得以重新启用。

既然跨域隔离能带来诸多好处,那为何不直接在所有网站上启用它,一劳永逸呢?要解答这个问题,我们首先得弄清楚启用它究竟意味着什么。

启用跨域隔离

要进入跨域隔离状态,主文档在提供服务时必须带有以下超文本传输协议(HTTP)标头:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

若操作正确,你可以借助window.crossOriginIsolated来检查页面是否处于跨域隔离状态。这里有一个示例,用以展示其工作原理。需要留意的是,你必须在单独的标签页中打开预览,稍后我们会对此进行探讨。

接下来,我们深入探究一下这两个标头的功能,以及它们会对网站产生怎样的影响。

跨域打开器策略(Cross-Origin-Opener-Policy)

在顶级文档上启用COOP:same-origin(跨域同源策略)标头后,会为从该文档打开的窗口创建一个独立的浏览上下文组,除非这些窗口与该文档源相同,并且设置了相同的COOP策略。由于存在独立的浏览上下文组,两个窗口之间的相互通信会被禁用。

下面通过一个具体场景来举例说明。

浏览器跨域隔离

假设有一个设置了COOP: same-origin的顶层文档,该文档打开了两个弹出窗口。其中一个弹出窗口的源是stackblitz.com,与顶层文档相同。基于源相同这一条件,它们处于同一个浏览上下文组中,所以这两个文档能够相互交互。然而,另一个弹出窗口的源是assets.stackblitz.com,与顶层文档不同,它会位于单独的浏览上下文组里。此时,这两个文档之间无法交互,因为在该弹出窗口中,window.opener的值会是null,并且开启者引用的.closed属性始终会返回true

这里有一个示例展示了COOP: same-origin的效果 。点击“Same origin”按钮,会打开一个与主文档同源的页面,主文档能够检测用户何时关闭了这个弹出窗口。但当打开跨域页面时,window.closed的值会是true,开启者无法得知页面何时真正关闭。

除了COOP: same-origin,还可以使用COOP: cross-origin,它允许与跨域文档进行交互,即能够检查窗口是否关闭。不过,若要让页面实现跨域隔离,必须将Cross-Origin-Opener-Policy头设置为same-origin。这个示例展示了cross-origin设置 的实际效果,点击“Cross origin”按钮时,顶层文档能够检测到弹出窗口的状态。

需要跨域窗口交互的操作,例如OAuth和支付等,都会受到影响。目前,业界正在讨论探索允许跨域隔离的方法 ,计划通过COOP: same-origin-allow-popups来实现。但截至本文撰写时,这一方案似乎不太可能在短期内落实。

诸如联邦身份识别 (之前称为WebID),或者Web支付 这样的API,最终可能会为开发者提供一种无需弹出窗口就能构建可靠OAuth或支付流程的方法,从而解决这个问题。不过,目前这些API还不够成熟。

如果你正受此问题困扰,在解决方案正式推出前,可以注册一个Origin TrialsOrigin Trials是一种临时实验性质的机制,它为你的网站提供临时例外,让你有更多时间进行必要的调整。但需要注意的是,其他浏览器,比如Firefox或Safari,并没有类似机制。在Chrome中,Origin Trials在问题解决之前不会被移除。

既然已经探讨了COOP头,接下来看看Cross-Origin-Embedder-Policy头,即COEP头。

跨域嵌入策略

COEP头决定了能否加载跨域资源。目前,Cross-Origin-Embedder-Policy唯一有效的值为require-corp。当文档带有此头时,跨域资源只有在获得明确授权的情况下才能被加载,若资源未获授权,加载操作将被阻止。

授权可通过两种不同方式达成。最常见的方式是借助CORS为特定域名授予加载该资源的权限。通过设置Access-Control-Allow-Origin头,资源能够为特定源赋予加载权限。第二种方式是通过Cross-Origin-Resource-Policy,即CORP头。当为资源设置Cross-Origin-Resource-Policy: cross-origin时,意味着授予所有跨域文档加载该资源的权限。

跨域加载资源

这张图片展示了一个网页嵌入了来自不同源的多个第三方资源。其中,JavaScript文件设置了CORP头为cross-origin,这表明我们的网站能够加载它。图片虽未设置CORP头,但通过CORS授予了我们网站加载它的权限。而视频资源设置了CORP头为same-origin,这使得加载该资源的操作被阻止。

如果外部资源,如图片、脚本、样式表或视频启用了CORS,那么必须通过使用crossOrigin属性进行显式加载。该属性的默认值是anonymous,这意味着只有当资源从相同源获取时,诸如cookies或http认证等凭证才会被发送。若图片从不同来源加载,所有信息都会被剥离,仅发送公开可用的信息。

我们来看一个示例,它展示了顶层文档带有COEP: require-corp头时嵌入图片的影响 。该示例呈现了我们在上图中看到的情况。只有在启用CORS并结合crossorigin图像属性,或者图片本身通过CORP: cross-origin头允许加载时,图片才会被加载。

使用Cross-Origin-Embedder-Policy的服务文档,也会对通过iframe加载的页面产生影响。若顶层文档带有COEP: require-corp,那么在iframe中加载的文档同样应当具备适当的头。此示例展示了在iframe中加载的具有适当头的文档 。顶层文档设置了COEP头,并嵌入了两个iframe。第一个iframe加载的是stackblitz.com ,它没有授予我们的文档加载它的权限。第二个iframe加载的是一个本地文件,与顶层文档同源。然而,由于顶层文档带有COEP,被加载的文档同样需要设置COEP头。若该文档不需要COEP头,它将获得提升的权限,从而能够加载顶层页面本身无法加载的资源。所以,通过iframe加载的同源文档也需要COEP头。

最后同样关键的是,Web Workers中的importScripts() 也受到此策略的影响。如果脚本从不同来源加载,必须使用Cross-Origin-Resource-Policy: cross-origin头来提供服务。不过,Web Worker中的importScripts()与使用script标签导入脚本存在差异。如前所述,通过script标签加载的跨源脚本,也可以通过CORS头结合crossorigin属性来授予权限。而对于importScriptsCORP头是授予权限的唯一方式,因为importScripts()没有启用CORScrossorigin属性。

跨域隔离的实践

现在我们已经探讨了如何启用跨源隔离,解释为什么在实际操作中并不总是那么容易,就变得更简单了。对于一些网站而言,这可能只需要开启这些头文件即可,但对于其他网站,包括StackBlitz,还有更多需要考量的因素。

我们来回顾一下应用这些头文件所面临的困难及其对StackBlitz的影响。

COOP破坏了OAuth

Cross-Origin-Opener-Policy: same-origin头强制将不同源分在单独的浏览上下文组。这意味着不同浏览上下文组之间无法进行通信。然而,诸如OAuth流程或支付等操作,确实需要跨源窗口交互。在撰写本文时,针对这一特定问题尚无解决方案。

不幸的是,对于StackBlitz来说,我们使用GitHub OAuth作为身份验证流程。一个可行的解决办法是将用户重定向到一个非跨源隔离的页面。该页面可以处理认证流程,然后将用户重定向回原页面。如果我们想要实现更无缝的集成,就必须依赖浏览器供应商来解决这个问题。

外部资源

由于COEP: require-corp头的存在,从诸如资产服务器等不同源加载资源,现在需要额外的代码更改。资产服务器必须明确授予权限,你的网站才能加载这些资源。如果没有授予权限,就无法加载这些资源,它们也就无法使用。我们在示例图片中看到了这个问题,该示例提供了带有COEP的顶层文档

作为应用程序开发人员,我们并非总能完全掌控资产的来源。有时,解决问题的唯一办法是通过我们自己的服务器代理这些资源,并添加适当的头文件。

这对StackBlitz的影响更为显著。我们能够控制大多数被我们网站加载的资源。然而,我们无法控制用户在开发应用程序时加载的资源。这意味着如果我们开启这些头文件,一些应用程序可能会出现故障。为了解决这个问题,我们首先需要了解问题所在,然后进行额外的更改,例如添加crossorigin图片属性(如果应用程序是本地开发的,则无需此操作)。

嵌入

通过iframe将跨源隔离的网站嵌入到未进行跨源隔离的上下文环境中,同样无法实现预期效果。若iframe中的文档,其某个上级并非处于跨源隔离状态,那么该文档也不会是跨源隔离的。这就表明,倘若你的网站依赖SharedArrayBuffer或其他需要跨源隔离的API,那么在被嵌入时,网站将无法正常运作。

我们来看一个关于跨源隔离嵌入的示例 。记得在单独的窗口中打开预览。你会发现,顶层文档未进行跨源隔离,因为它没有设置所需的头信息。然而,iframe文档通过设置头文件实现了跨源隔离。但由于它被嵌入在一个非跨源隔离的上下文里,所以iframe本身也并非跨源隔离状态。在示例项目中,你可以点击iframe中的链接,在新窗口中打开它,此时便能看到它处于跨源隔离状态。

如果StackBlitz启用了这些头文件,情况便会如此。所有依赖嵌入StackBlitz WebContainers示例的网站,现在都会出现问题,除非确保它们自身也是跨源隔离的。

使用credentialless进行跨源隔离

在大规模部署中启用跨源隔离,事实证明颇具难度。原因在于,所有子资源都必须通过授予权限来明确选择加入,而开发者并非总能掌控这些子资源。对于某些网站而言,这或许可行,但对于像论坛、Google Earth以及StackBlitz这类包含用户生成内容的网站来说,就会引发依赖问题。Google在大规模切换到跨源隔离时,也深切体会到了这种困扰。因此,他们提出了一个新的提案:Cross-Origin-Embedder-Policy: credentialless

根据新提案,你可以通过以下头文件来启用跨源隔离:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless

credentialless提案的核心思路是反转加载资源的决策方式。使用require-corp时,由资源本身决定是否可通过明确授予权限来加载。而使用credentialless时,则由网站决定如何加载外部资源。

若将COEP头设置为credentialless,跨源请求在未启用CORS的情况下,会剥离所有凭据,比如cookies或HTTP认证信息。这意味着请求的跨源服务器无法响应敏感资源,响应中仅会包含公开可用的信息。

使用`credentialless`进行跨源隔离

如图片所示,若加载的资源将Cross-Origin-Resource-Policy设置为cross-origin,那么凭据不会被剥离。这是因为资源明确授予了权限,将自身标记为可被其他来源安全加载,所以不会共享敏感信息。若该值被设置为same-origin,资源则完全无法加载。要是没有设置CORPCORS头,请求虽会被允许,但凭据会被剥离。

这是之前提到的图片示例,不过现在顶层文档是通过COEP: credentialless提供服务的。如你所见,在使用require-corp的示例 中,前三个图片根本没有加载出来。而切换到credentialless后,只有一种情况会失败,原因是crossorigin属性强制实施了CORS。

这看似是个更好的选择,毕竟它无需对服务器或应用程序做出更改。可惜的是,credentialless目前还未得到广泛支持。

credentialless支持情况

在撰写本文时,Cross-Origin-Embedder-Policy: credentialless仅在基于Chromium的浏览器中可用,并且是从Chrome 96版本开始推出的。Firefox已决定着手开发该功能,但尚未给出具体的可用时间预估。据我们了解,Safari团队不打算实现此功能,这着实令人遗憾,因为这会阻碍跨源隔离在更大范围内的实施。

结论

StackBlitz目前仅能在基于Chromium的浏览器上运行,并非因为某些特定于Chrome的API。跨源隔离问题所涉及的机制极为复杂且脆弱。所有设置都必须准确无误,否则就可能导致某些功能无法正常使用。但正如我们之前看到的,像论坛、Google Earth以及StackBlitz这类提供用户生成内容的网站,在部署跨源隔离时都会面临难题。

StackBlitz如今能在基于Chromium的桌面浏览器上运行,是因为我们采用了源试验 。而其他所有浏览器,甚至包括Android版Chrome,都需要借助上述机制来实现跨源隔离。

源试验正是当前嵌入基于StackBlitz WebContainers的项目,无需你进行特定配置就能正常运作的原因。同时,它也是你能够加载任意资源,而无需代理服务器或指定正确头文件的原因。跨源隔离是个复杂的问题,我们不能指望所有用户都了解其含义,或者知道如何解决相关问题。

未来计划

理想状况下,所有浏览器都能支持新的credentialless模式。但由于我们不想一直等待,目前正在考虑基于用户浏览器采用一种混合方法。如果用户使用的是支持credentialless的浏览器,我们可以在credentialless模式下提供StackBlitz服务。在其他情况下,我们则可以回退到require-corp模式。

然而,使用不支持credentialless浏览器的用户,可能会受到上述问题的影响。不过,有一定程度的支持总比完全没有要好。

我们还需要解决其他一些问题,以实现完全一致的跨浏览器支持。这些问题超出了本文的讨论范围,因为所有这些问题都是能够解决的。但是,如果没有浏览器供应商的支持以及实施credentialless的意愿,将会有越来越多的网站受到影响。

总结

  • 由于2018年初出现的Spectre漏洞,SharedArrayBuffer以及其他一些功能在所有浏览器中都被禁用了。借助新的安全机制,通过跨源隔离可以重新启用它们。
  • 将文档与Cross-Origin-Opener-PolicyCross-Origin-Embedder-Policy头一起提供,就能使文档实现跨源隔离。
  • 鉴于其存在的限制,跨源隔离目前很难在大规模范围内推广应用。OAuth流程会受到破坏,包含未知来源用户生成内容的网站,若不进行额外更改,将无法加载资源。
  • 嵌入跨源隔离页面时,始终要求顶层页面也处于跨源隔离状态。目前还没有允许非跨源隔离网站嵌入跨源隔离网站的提案。
  • 新的credentialless提案 几乎可以解决所有问题,但目前仅在基于Chromium的浏览器中可用。Firefox正在开发该功能 ,据我们所知,Safari暂无实现此功能的计划
  • StackBlitz正在研究基于用户浏览器的混合方法。不过,不支持credentialless的浏览器,其用户体验的功能将会根据实际情况受到一定限制。

参考

https://web.dev/coop-coep/ https://web.dev/why-coop-coep/ https://chromestatus.com/feature/4918234241302528

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件举报,一经查实,本站将立刻删除。

文章由技术书栈整理,本文链接:https://study.disign.me/article/202509/17.cross-browser-with-coop-coep.md

发布时间: 2025-02-27

原文链接: https://blog.stackblitz.com/posts/cross-browser-with-coop-coep/