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 orperformance.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.
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.
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.
crossOriginIsolatedfalse 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. Trycredentialless. - Popups lose
window.opener. COOPsame-originsevers it. Usesame-origin-allow-popupswhere 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.
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.
Related
- Detecting cross-origin isolation at runtime — choosing the right build in code.
- Using Atomics for Wasm thread synchronization — what the restored capabilities are used for.
- Measuring memory with measureUserAgentSpecificMemory — another API gated on isolation.
- Serving Wasm over HTTPS on localhost — the secure context isolation also requires.
← Back to Browser Sandbox & Security Boundaries