Spectre and Cross-Origin Isolation for Wasm

This page answers one question: why does a WebAssembly app need special headers to use threads and precise timers, what attack are those headers defending against, and what does enabling them cost the rest of the page?

Prerequisites

  • [ ] A WebAssembly app that uses, or wants to use, SharedArrayBuffer, Wasm threads or performance.measureUserAgentSpecificMemory.
  • [ ] Control over the response headers of the page and its subresources.
  • [ ] A list of third-party resources the page embeds: images, fonts, iframes, scripts.

What Spectre changed for the web

Spectre is a family of CPU vulnerabilities disclosed in 2018. Modern processors execute instructions speculatively, before they know whether those instructions should run, and discard the results if the guess was wrong. The results are discarded, but their side effects on the CPU caches are not. A program that can measure how long memory accesses take can infer which addresses were cached, and from that, data the speculative execution touched — including data the program was never allowed to read.

In a browser, that means a malicious page could potentially read memory belonging to another site that shares its process — a cross-origin image, an iframe, a response the page fetched without permission to read. The attack needs a precise clock. Browsers responded in two ways: they coarsened performance.now(), and they disabled SharedArrayBuffer, because a worker incrementing a counter in shared memory is an extremely precise clock. WebAssembly threads depend on shared memory, so they went with it.

Cross-origin isolation is how browsers brought those capabilities back safely. A page that opts in guarantees that its process contains only resources that agreed to be there. With nothing secret from another origin in the process, a timing attack has nothing to steal, and the browser can re-enable shared memory and precise timers.

How cross-origin isolation contains a timing attack Without isolation, a page's process can contain cross-origin data, so precise timers and shared memory are withheld. With COOP and COEP, the process holds only the page's own content and resources that opted in, so the browser can safely grant SharedArrayBuffer, Wasm threads and fine-grained timers. COOP same-origin no shared browsing context with cross-origin windows COEP require-corp / credentialless every subresource opted in, or loaded without credentials process holds only consenting data nothing secret from other origins to leak capabilities restored SharedArrayBuffer, Wasm threads, 5 µs timers, memory measurement

Step 1 — check where your page stands

console.table({
  crossOriginIsolated: self.crossOriginIsolated,
  sab: typeof SharedArrayBuffer === "function",
  timerResolution: (() => {
    let a = performance.now(), b;
    while ((b = performance.now()) === a);
    return (b - a).toFixed(3) + " ms";
  })(),
});

On a non-isolated page, crossOriginIsolated is false, SharedArrayBuffer may be undefined, and the timer resolution is around 0.1 ms. On an isolated page, the resolution is about 0.005 ms and threads work.

Step 2 — send the two headers

Isolation requires two response headers on the top-level document:

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

Cross-Origin-Opener-Policy: same-origin puts the page in its own browsing context group, severing window.opener relationships with cross-origin windows so they cannot share a process. Cross-Origin-Embedder-Policy: require-corp requires every subresource the page loads to explicitly allow being embedded — either through CORS or a Cross-Origin-Resource-Policy header. The server setup for common dev servers is in configuring COOP/COEP headers for SharedArrayBuffer.

Step 3 — fix the subresources that break

require-corp is strict. An image from another origin without CORP or CORS headers now fails to load, and so does a font, an iframe or a script. Same-origin resources are unaffected. For third-party resources there are three options:

<!-- 1. ask for CORS, if the third party supports it -->
<img src="https://images.example.net/photo.jpg" crossorigin="anonymous">

<!-- 2. the third party sends: Cross-Origin-Resource-Policy: cross-origin -->

<!-- 3. switch COEP to credentialless (see step 4) -->

Your own CDN and asset hosts should send Cross-Origin-Resource-Policy: cross-origin (or same-site) on everything, including the .wasm files. Iframes need the embedded page to send its own COEP header, which many third-party embeds — video players, maps, payment forms — do not.

What COEP require-corp does to common subresources How typical resources behave on a page with COEP require-corp, and what each needs in order to load. resource same-origin cross-origin needs .wasm, .js, workers loads CORS or CORP header images, fonts loads crossorigin + CORS, or CORP iframes loads embedded page must send COEP third-party scripts loads CORS or CORP header

Step 4 — consider credentialless

Cross-Origin-Embedder-Policy: credentialless relaxes the rule for resources loaded without credentials. Cross-origin no-cors requests are sent without cookies, so they cannot reveal user-specific data, and the browser lets them load even without CORP headers:

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

Public images, fonts and scripts from CDNs typically work unchanged. Resources that require the user’s cookies to load — an authenticated image from another origin — do not. For most Wasm apps credentialless is the easier path to isolation; check that every browser you support implements it, and fall back to a single-threaded build where it does not.

Step 5 — plan for pages that cannot be isolated

Some pages cannot adopt COOP: a page that opens a payment provider or an OAuth popup and needs to talk to it through window.opener loses that channel under same-origin. Cross-Origin-Opener-Policy: same-origin-allow-popups keeps popups you open working, at the cost of not being isolated in some browsers. When isolation is impossible on a page, isolate a different page — run the threaded Wasm work on a dedicated route — or ship a single-threaded build and detect which to load, as in handling browsers without SharedArrayBuffer.

Rolling isolation out on an existing site

Adopting isolation on a site that already has users and third-party integrations is mostly an inventory exercise, and doing it in the right order avoids a broken release. Start by deploying COEP in report-only mode — Cross-Origin-Embedder-Policy-Report-Only: require-corp with a reporting endpoint — which logs every subresource that would be blocked without blocking anything. A week of real traffic produces the definitive list of what needs fixing, including resources that only load for some users, such as regional payment widgets or A/B-tested scripts.

Work through that list by category. Resources you host get a Cross-Origin-Resource-Policy header at the CDN. Third-party resources that support CORS get the crossorigin attribute. Third parties that support neither are candidates for credentialless, for proxying through your own origin, or for removal. Iframes from third parties are usually the hardest; check whether the provider offers an isolation-compatible embed, and if not, consider whether the threaded Wasm feature can live on a different page from the embed.

Then enable COOP and COEP together on a single route first — the page that actually needs threads — and watch error rates and the back/forward cache metrics before widening it. Isolation is a property of a page, not of a site, and there is rarely a reason to isolate pages that do not use the capabilities it unlocks.

What isolation does not do

It is worth being precise about the guarantee. Cross-origin isolation does not make WebAssembly code safer, does not protect your own page’s data from your own code, and does not stop a malicious module you chose to load. It only ensures that cross-origin data is not present in your process, so that the precise timers and shared memory the browser grants you cannot be used to read it. Inside the module, the usual sandbox rules apply: linear memory isolation, explicit imports and the browser’s same-origin policy. Within the page, your security still depends on what you load and what you allow it to call — the concerns covered in restricting what a module can import.

Expected output

On an isolated page, the check from step 1 prints:

┌─────────────────────┬────────────┐
│ crossOriginIsolated │ true       │
│ sab                 │ true       │
│ timerResolution     │ '0.005 ms' │
└─────────────────────┴────────────┘

and a threaded Wasm build starts its worker pool without errors.

Gotchas

  • Headers set on the page, not on the worker or the iframe. Workers inherit isolation from their creator, but must still be loadable under COEP; iframes need their own headers.
  • crossOriginIsolated false despite the headers. A redirect stripped them, or they were set on an HTML file served through a different path. Check the final response in the Network panel.
  • Analytics and ads stop working. Third-party scripts and pixels without CORP or CORS fail under require-corp. Try credentialless.
  • Popups lose window.opener. COOP same-origin severs it. Use same-origin-allow-popups where flows depend on it.

Performance note

Isolation can affect performance indirectly. Isolated pages get their own process, which costs memory — tens of megabytes on desktop — and browsers may disable the back/forward cache for some isolated pages, making back navigation slower. In return, threaded Wasm work becomes possible: an image pipeline that took 840 ms single-threaded took 260 ms with four threads on the same phone.

What isolation enabled for one image pipeline An image processing pipeline on a mid-range phone, single-threaded on a non-isolated page and with four Wasm threads on an isolated page. ms per processed image not isolated, single-threaded build 840 ms isolated, 4 threads 260 ms

Frequently Asked Questions

Is Spectre still a real threat? Mitigations in CPUs, operating systems and browsers have reduced it, but browsers keep the defensive architecture — site isolation, coarse timers, opt-in isolation — because new variants continue to appear.

Do server-side Wasm runtimes need this? Not in the same form. Runtimes such as wasmtime isolate tenants with separate instances and process-level controls; COOP and COEP are browser concepts.

Does isolation affect postMessage? Messages between same-origin windows and workers are unaffected. Cross-origin windows lose access to each other through opener, and SharedArrayBuffer can only be posted within the isolated agent cluster.

Can a Chrome origin trial replace the headers? Temporary origin trials existed during the transition. The headers are the durable mechanism and the only one to rely on.

← Back to Browser Sandbox & Security Boundaries