Site Isolation for web developers

Site Isolation for web developers

Site Isolation for web developers

Chrome 67 on desktop has a new feature called Site Isolation enabled by default. This article explains what Site Isolation is all about, why it’s necessary, and why web developers should be aware of it.

What is Site Isolation?

The internet is for watching cat videos and managing cryptocurrency wallets, amongst other things — but you wouldn’t want fluffycats.example to have access to your precious cryptocoins! Luckily, websites typically cannot access each other’s data inside the browser thanks to the Same-Origin Policy. Still, malicious websites may try to bypass this policy to attack other websites, and occasionally, security bugs are found in the browser code that enforces the Same-Origin Policy. The Chrome team aims to fix such bugs as quickly as possible.

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. As a result, with Site Isolation it’s much more difficult for a malicious website to use speculative side-channel attacks like Spectre to steal data from other sites. As the Chrome team finishes additional enforcements, Site Isolation will also help even when an attacker’s page can break some of the rules in its own process.

Site Isolation effectively makes it harder for untrusted websites to access or steal information from your accounts on other websites. It offers additional protection against various types of security bugs, such as the recent Meltdown and Spectre side-channel attacks.

For more details on Site Isolation, see our article on the Google Security blog.

Cross-Origin Read Blocking

Even when all cross-site pages are put into separate processes, pages can still legitimately request some cross-site subresources, such as images and JavaScript. A malicious web page could use an <img> element to load a JSON file with sensitive data, like your bank balance:

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

Without Site Isolation, the contents of the JSON file would make it to the memory of the renderer process, at which point the renderer notices that it’s not a valid image format and doesn’t render an image. But, the attacker could then exploit a vulnerability like Spectre to potentially read that chunk of memory.

Instead of using <img>, the attacker could also use <script> to commit the sensitive data to memory:

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

Cross-Origin Read Blocking, or CORB, is a new security feature that prevents the contents of balance.json from ever entering the memory of the renderer process memory based on its MIME type.

Let’s break down how CORB works. A website can request two types of resources from a server:

  1. data resources such as HTML, XML, or JSON documents
  2. media resources such as images, JavaScript, CSS, or fonts

A website is able to receive data resources from its own origin or from other origins with permissive CORS headers such as Access-Control-Allow-Origin: *. On the other hand, media resources can be included from any origin, even without permissive CORS headers.

CORB prevents the renderer process from receiving a cross-origin data resource (i.e. HTML, XML, or JSON) if:

  • the resource has an X-Content-Type-Options: nosniff header
  • CORS doesn’t explicitly allow access to the resource

If the cross-origin data resource doesn’t have the X-Content-Type-Options: nosniff header set, CORB attempts to sniff the response body to determine whether it’s HTML, XML, or JSON. This is necessary because some web servers are misconfigured and serve images as text/html, for example.

Data resources that are blocked by the CORB policy are presented to the process as empty, although the request does still happen in the background. As a result, a malicious web page has a hard time pulling cross-site data into its process to steal.

For optimal security and to benefit from CORB, we recommend the following:

  • Mark responses with the correct Content-Type header. (For example, HTML resources should be served as text/html, JSON resources with a JSON MIME type and XML resources with an XML MIME type).
  • Opt out of sniffing by using the X-Content-Type-Options: nosniff header. Without this header, Chrome does do a quick content analysis to try to confirm that the type is correct, but since this errs on the side of allowing responses through to avoid blocking things like JavaScript files, you’re better off affirmatively doing the right thing yourself.

For more details, refer to the CORB for web developers article or our in-depth CORB explainer.

Why should web developers care about Site Isolation?

For the most part, Site Isolation is a behind-the-scenes browser feature that is not directly exposed to web developers. There is no new web-exposed API to learn, for example. In general, web pages shouldn’t be able to tell the difference when running with or without Site Isolation.

However, there are some exceptions to this rule. Enabling Site Isolation comes with a few subtle side-effects that might affect your website. We maintain a list of known Site Isolation issues, and we elaborate on the most important ones below.

Full-page layout is no longer synchronous

With Site Isolation, full-page layout is no longer guaranteed to be synchronous, since the frames of a page may now be spread across multiple processes. This might affect pages if they assume that a layout change immediately propagates to all frames on the page.

As an example, let’s consider a website named fluffykittens.example that communicates with a social widget hosted on social-widget.example:

<!-- 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>

At first, the social widget’s <iframe>’s width is 123 pixels. But then, the FluffyKittens page changes the width to 456 pixels (triggering layout) and sends a message to the social widget, which has the following code:

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

Whenever the social widget receives a message through the postMessage API, it logs the width of its root <html> element.

Which width value gets logged? Before Chrome enabled Site Isolation, the answer was 456. Accessing document.documentElement.clientWidth forces layout, which used to be synchronous before Chrome enabled Site Isolation. However, with Site Isolation enabled, the cross-origin social widget re-layout now happens asynchronously in a separate process. As such, the answer can now also be 123, i.e. the old width value.

If a page changes the size of a cross-origin <iframe> and then sends a postMessage to it, with Site Isolation the receiving frame may not yet know its new size when receiving the message. More generally, this might break pages if they assume that a layout change immediately propagates to all frames on the page.

In this particular example, a more robust solution would set the width in the parent frame, and detect that change in the <iframe> by listening for a resize event.

Key Point: In general, avoid making implicit assumptions about browser layout behavior. Full-page layout involving cross-origin <iframe>s was never explicitly specified to be synchronous, so it’s best to not write code that relies on this.

Unload handlers might time out more often

When a frame navigates or closes, the old document as well as any subframe documents embedded in it all run their unload handler. If the new navigation happens in the same renderer process (e.g. for a same-origin navigation), the unload handlers of the old document and its subframes can run for an arbitrarily long time before allowing the new navigation to commit.

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

In this situation, the unload handlers in all frames are very reliable.

However, even without Site Isolation some main frame navigations are cross-process, which impacts unload handler behavior. For example, if you navigate from old.example to new.example by typing the URL in the address bar, the new.example navigation happens in a new process. The unload handlers for old.example and its subframes run in the old.example process in the background, after the new.example page is shown, and the old unload handlers are terminated if they don’t finish within a certain timeout. Because the unload handlers may not finish before the timeout, the unload behavior is less reliable.

Note: Currently, DevTools support for unload handlers is largely missing. For example, breakpoints inside of unload handlers don’t work, any requests made during unload handlers don’t show up in the Network pane, any console.log calls made during unload handlers may not show up, etc. Star Chromium issue #851882 to receive updates.

With Site Isolation, all cross-site navigations become cross-process, so that documents from different sites don’t share a process with each other. As a result, the above situation applies in more cases, and unload handlers in <iframe>s often have the background and timeout behaviors described above.

Another difference resulting from Site Isolation is the new parallel ordering of unload handlers: without Site Isolation, unload handlers run in a strict top-down order across frames. But with Site Isolation, unload handlers run in parallel across different processes.

These are fundamental consequences of enabling Site Isolation. The Chrome team is working on improving the reliability of unload handlers for common use cases, where feasible. We’re also aware of bugs where subframe unload handlers aren’t yet able to utilize certain features and are working to resolve them.

An important case for unload handlers is to send end-of-session pings. This is commonly done as follows:

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

Note: We recommend to use the pagehide event over beforeunload or unload, for reasons unrelated to Site Isolation.

A better approach that is more robust in light of this change is to use navigator.sendBeacon instead:

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

If you need more control over the request, you can use the Fetch API’s keepalive option:

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

Conclusion

Site Isolation makes it harder for untrusted websites to access or steal information from your accounts on other websites by isolating each site into its own process. As part of that, CORB tries to keep sensitive data resources out of the renderer process. Our recommendations above ensure you get the most out of these new security features.

Thanks to Alex Moshchuk, Charlie Reis, Jason Miller, Nasko Oskov, Philip Walton, Shubhie Panicker, and Thomas Steiner for reading a draft version of this article and giving their feedback.

This post is also available in: Norsk bokmål