适用于 Web 开发者的网站隔离功能

Mathias Bynens
Mathias Bynens

桌面版 Chrome 67 中有一项名为网站隔离的新功能,默认处于启用状态。本文介绍了网站隔离的含义、必要性,以及 Web 开发者为何应加以重视。

什么是网站隔离?

互联网可用于观看猫咪视频和管理加密货币钱包等用途,但您肯定不希望 fluffycats.example 访问您珍贵的加密货币!幸运的是,由于同源政策,网站通常无法在浏览器中访问彼此的数据。不过,恶意网站可能会尝试绕过此政策来攻击其他网站,并且在强制执行同源政策的浏览器代码中偶尔也会发现安全 bug。Chrome 团队的目标是尽快修复此类 bug。

网站隔离是 Chrome 的一项安全功能,可提供额外的防御层,降低此类攻击成功的可能性。该功能可确保来自不同网站的网页始终放入不同的进程,每个进程都在沙盒中运行,沙盒会限制进程可执行的操作。该功能还会阻止进程从其他网站接收特定类型的敏感数据。因此,启用网站隔离功能后,恶意网站将更难以使用 Spectre 等推测性侧信道攻击从其他网站窃取数据。随着 Chrome 团队完成更多强制执行措施,即使攻击者的网页可以在自己的进程中违反某些规则,网站隔离功能也能发挥作用。

网站隔离功能可有效地降低不受信任的网站访问或窃取您在其他网站上的账号信息的风险。它可针对各种类型的安全 bug(例如近期的 Meltdown 和 Spectre 边信道攻击)提供额外的保护。

如需详细了解网站隔离,请参阅 Google 安全博客上的文章

跨源读取屏蔽

即使将所有跨网站网页放入单独的进程中,网页仍可以合法地请求某些跨网站子资源,例如图片和 JavaScript。恶意网页可能会使用 <img> 元素加载包含敏感数据(例如您的银行余额)的 JSON 文件:

<img src="https://your-bank.example/balance.json" />
<!-- Note: the attacker refused to add an `alt` attribute, for extra evil points. -->

如果没有网站隔离,JSON 文件的内容会进入渲染程序进程的内存,此时渲染程序会注意到它不是有效的图片格式,并且不会渲染图片。但是,攻击者随后可能会利用 Spectre 等漏洞来读取该内存块。

攻击者还可以使用 <script> 将敏感数据提交到内存,而不是使用 <img>

<script src="https://your-bank.example/balance.json"></script>

跨源读取屏蔽 (CORB) 是一项新安全功能,可根据 balance.json 的 MIME 类型阻止其内容进入渲染程序进程内存。

我们来详细了解一下 CORB 的运作方式。网站可以向服务器请求两种类型的资源:

  1. 数据资源,例如 HTML、XML 或 JSON 文档
  2. 媒体资源,例如图片、JavaScript、CSS 或字体

网站能够从自己的来源或具有宽松 CORS 标头(例如 Access-Control-Allow-Origin: *)的其他来源接收数据资源。另一方面,媒体资源可以从任何来源添加,即使没有宽松的 CORS 标头也是如此。

如果满足以下条件,CORB 会阻止渲染器进程接收跨源数据资源(即 HTML、XML 或 JSON):

  • 资源具有 X-Content-Type-Options: nosniff 标头
  • CORS 未明确允许访问资源

如果跨源数据资源未设置 X-Content-Type-Options: nosniff 标头,CORB 会尝试嗅探响应正文,以确定其是 HTML、XML 还是 JSON。这是必要的,因为某些 Web 服务器配置有误,例如以 text/html 形式提供图片。

被 CORB 政策屏蔽的数据资源会以空的形式呈现给进程,但请求仍会在后台发生。因此,恶意网页很难将跨站数据拉取到其进程中进行窃取。

为了实现最佳安全性并受益于 CORB,我们建议您采取以下措施:

  • 使用正确的 Content-Type 标头标记响应。(例如,HTML 资源应以 text/html 的形式提供,JSON 资源应采用 JSON MIME 类型,XML 资源应采用 XML MIME 类型)。
  • 使用 X-Content-Type-Options: nosniff 标头停用嗅探。如果没有此标头,Chrome 会进行快速内容分析,以尝试确认类型是否正确,但由于此操作会出于避免屏蔽 JavaScript 文件等内容的考虑而允许响应通过,因此您最好自行确认正确的类型。

如需了解详情,请参阅面向 Web 开发者的 CORB 文章我们的深入 CORB 说明

为什么 Web 开发者应关注网站隔离?

在大多数情况下,网站隔离是一项幕后浏览器功能,不会直接向 Web 开发者公开。例如,无需学习新的公开 Web API。一般来说,网页在启用或停用网站隔离功能时,用户应该无法察觉到差异。

不过,这项规则也有一些例外情况。启用网站隔离会带来一些细微的副作用,可能会影响您的网站。我们维护着已知网站隔离问题列表,并在下文中详细介绍了最重要的问题。

整页布局不再同步

启用网站隔离后,我们无法再保证整个页面的布局是同步的,因为页面的帧现在可能会分布在多个进程之间。如果网页假定布局更改会立即传播到网页上的所有帧,这可能会影响网页。

例如,假设有一个名为 fluffykittens.example 的网站与托管在 social-widget.example 上的社交 widget 进行通信:

<!-- https://fluffykittens.example/ -->
<iframe src="https://social-widget.example/" width="123"></iframe>
<script>
  const iframe = document.querySelector('iframe');
  iframe.width = 456;
  iframe.contentWindow.postMessage(
    // The message to send:
    'Meow!',
    // The target origin:
    'https://social-widget.example'
  );
</script>

最初,社交微件的 <iframe> 宽度为 123 像素。不过,FluffyKittens 页面会将宽度更改为 456 像素(触发布局),并向社交 widget 发送消息,该 widget 包含以下代码:

<!-- https://social-widget.example/ -->
<script>
  self.onmessage = () => {
    console.log(document.documentElement.clientWidth);
  };
</script>

每当社交 widget 通过 postMessage API 收到消息时,都会记录其根 <html> 元素的宽度。

系统会记录哪个宽度值?在 Chrome 启用网站隔离之前,答案为 456。访问 document.documentElement.clientWidth 会强制布局,在 Chrome 启用网站隔离之前,布局是同步的。不过,启用网站隔离功能后,跨源社交 widget 重新布局现在会在单独的进程中异步进行。因此,答案现在也可以是 123,即旧的 width 值。

如果某个网页更改了跨源 <iframe> 的大小,然后向其发送 postMessage,在启用网站隔离的情况下,接收框架在接收消息时可能还不知道其新大小。更一般地说,如果网页假定布局更改会立即传播到网页上的所有框架,则可能会导致网页出现问题。

在本示例中,更稳健的解决方案是在父级帧中设置 width,并通过监听 resize 事件在 <iframe> 中检测该更改。

卸载处理脚本可能会更频繁地超时

当帧导航或关闭时,旧文档以及其中嵌入的所有子帧文档都会运行其 unload 处理脚本。如果新导航发生在同一渲染程序中(例如,对于同源导航),旧文档及其子帧的 unload 处理脚本可能会运行很长时间,然后才允许新导航提交。

addEventListener('unload', () => {
  doSomethingThatMightTakeALongTime();
});

在这种情况下,所有帧中的 unload 处理程序都非常可靠。

不过,即使没有网站隔离,某些主框架导航也是跨进程的,这会影响卸载处理脚本行为。例如,如果您通过在地址栏中输入网址从 old.example 导航到 new.example,则 new.example 导航会在新进程中进行。old.example 及其子帧的卸载处理脚本会在 new.example 页面显示后在后台的 old.example 进程中运行,如果旧的卸载处理脚本未在特定超时时间内完成,则会被终止。由于卸载处理脚本可能无法在超时之前完成,因此卸载行为的可靠性较低。

启用网站隔离后,所有跨网站导航都会变为跨进程导航,这样不同网站的文档就不会共用进程。因此,上述情况适用于更多情况,并且 <iframe> 中的卸载处理脚本通常具有上述后台和超时行为。

网站隔离带来的另一个不同之处是,卸载处理脚本的新并行排序:如果没有网站隔离,卸载处理脚本会在帧中按严格的从上到下的顺序运行。但是,启用网站隔离后,卸载处理脚本会在不同的进程中并行运行。

这些是启用网站隔离的基本后果。Chrome 团队正在努力提高常见用例的卸载处理脚本的可靠性(如有可能)。我们还发现了子帧卸载处理程序尚无法使用某些功能的 bug,并正在努力解决这些 bug。

卸载处理程序的一个重要用例是发送会话结束 ping。通常,可按如下方式执行此操作:

addEventListener('pagehide', () => {
  const image = new Image();
  img.src = '/end-of-session';
});

鉴于这一变化,更好且更稳健的方法是改用 navigator.sendBeacon

addEventListener('pagehide', () => {
  navigator.sendBeacon('/end-of-session');
});

如果您需要更好地控制请求,可以使用 Fetch API 的 keepalive 选项:

addEventListener('pagehide', () => {
  fetch('/end-of-session', {keepalive: true});
});

总结

网站隔离功能会将每个网站隔离到自己的进程中,从而使不受信任的网站更难以访问或窃取您在其他网站上的账号信息。为此,CORB 会尝试将敏感数据资源从渲染程序中排除。请按照上述建议操作,以便充分利用这些新的安全功能。

感谢 Alex Moshchuk、Charlie Reis、Jason Miller、Nasko Oskov、Philip Walton、Shubhie Panicker 和 Thomas Steiner 阅读本文的草稿并提供反馈。