Web Platform · Web Security Model

Spectre, Site Isolation, and the Limits of the Origin Boundary

Why speculative execution made the origin a weaker boundary than it appears, how process-per-site and response blocking reduce the exposure, and which timing channels remain open.

The same-origin policy is a software check inside the renderer. It decides whether an API returns a value to script; it does not decide what bytes are present in the process, and it does not decide what the processor speculatively reads while a branch is unresolved. Spectre demonstrated that a program can be made to speculatively execute past a bounds check and leave a trace of the out-of-bounds data in the cache, which a second, carefully timed sequence can recover. When the “out-of-bounds” region contains another origin’s document, the origin boundary is bypassed without any API being called.

Browsers cannot patch the processor. What they can do is change what shares an address space, what data is allowed into a process, and how precisely time can be measured. Those three changes are the browser-side answer to Spectre, and each has a visible cost.

What the attack needs from a browser

The published Spectre variants require three ingredients that a web page can supply.

First, a gadget: code that performs a bounds check the processor will mispredict, followed by a memory access indexed by the value that escaped. Any function with an array access and a branch qualifies in principle, which is why the attack is not tied to a specific bug in a specific engine. The JIT compiler makes this easier by producing code the attacker influences, and shared memory makes the timing loop far more precise.

Second, co-resident data: the secret must be in the same address space, because the cache is shared within a core but speculative loads cannot cross a page-table boundary for another process’s memory in the way the attack uses.

Third, a clock: the recovery step measures how long a load takes, so the attacker needs a timer whose resolution and noise floor allow many repeated probes to be aggregated into a distinguishable signal. Cache hits and misses differ by tens to a few hundred cycles, far below the resolution of any script-visible clock, which is why the attack repeats the probe rather than timing a single access.

The browser mitigation strategy maps one-to-one onto these: remove co-residence, prevent sensitive data from entering the process, and coarsen the clock.

Process-per-site changes what “same process” means

Before site isolation, a renderer process could host documents from many unrelated sites, because process assignment followed the navigation and reuse saved memory. Under site isolation, a process is assigned per site — scheme plus registrable domain — so https://a.example.com and https://b.example.com may share one renderer, while https://bank.example and https://news.example do not. A cross-site iframe gets its own process, and its script runs in an address space that contains only its own site’s documents.

The consequence for Spectre is that the attacker’s gadget and the victim’s document are no longer in the same address space for cross-site embeddings. Speculative loads can still reach the attacker’s own process memory, including other documents from the same site inside that process. That residual is why same-site boundaries remain weaker than cross-site ones: a compromised or attacker-controlled subdomain shares a process with its siblings unless something separates them further.

Origin-Agent-Cluster: ?1 is the finer-grained control. It keys the agent cluster by origin rather than by site, so a document that opts in does not share a JavaScript agent — and, where the browser supports it, a process — with sibling subdomains. It also disables the document.domain setter, which otherwise relaxes the origin boundary within the cluster. Site isolation is a site-level mitigation; origin-keyed clustering is the origin-level version, and it is the appropriate choice for any application that hosts multiple parties on subdomains.

Process separation is not free. Each renderer carries a fixed baseline of memory for the JavaScript heap, the layout engine, and the process itself, so a page with many cross-site frames costs proportionally more. Browsers cap the number of processes and fall back to sharing when the limit is reached, which reintroduces co-residence for pages with a very large frame tree. The protection is a policy with an implementation limit, not an invariant.

Keeping sensitive bytes out of the renderer

A process boundary is only useful if the attacker’s process does not contain the target’s data. Historically, an <img> or <script> tag pointing at a cross-origin URL would fetch the response into the embedding document’s process, where the bytes sat in memory even though script could not read them. A Spectre gadget could then read them speculatively.

Two mechanisms address this. CORS controls whether script may read a response; it does not prevent the bytes from being fetched. Cross-Origin Read Blocking goes further: when a no-cors request’s response is sniffed as HTML, JSON, or XML, and the request came from a cross-origin context without an explicit permission, the response body is replaced with an empty one before it reaches the requesting process. The classification uses the Content-Type and a content sniff, so a response labelled text/plain but containing HTML is still filtered.

The practical rules that follow are worth stating as deployment guidance. Serve JSON with Content-Type: application/json so that it is classified and blocked rather than delivered. Serve user-uploaded content with Content-Type: text/plain plus X-Content-Type-Options: nosniff if it must not be interpreted, and serve it from a separate origin if it must be. Add Cross-Origin-Resource-Policy to responses that are not meant to be embedded at all, which stops the fetch before the classification step. The header’s role in the isolation handshake is covered in cross-origin isolation with COOP and COEP.

Response blocking of this kind has limits that matter operationally. It cannot block responses that are legitimately used cross-origin: an image must reach the embedding process to be decoded and painted, a media file must reach the media stack. It relies on content type and sniffing, so a response that looks like an image but contains a document is handled imperfectly. And it does not apply to CORS-enabled responses at all, because those are already readable by the requesting origin — which is the correct semantics but means CORS is not a performance or memory control.

frame-ancestors and X-Frame-Options complement the process work by preventing the embedding entirely for documents that never need to be framed. A page that refuses framing cannot be attacked with the frame-based leak primitives described below, and it also cannot be clickjacked.

Clocks, shared memory, and the cross-origin isolation trade

Timer coarsening attacks the measurement step. For a document that is not cross-origin isolated, the specification requires performance.now() to be clamped to a coarse granularity — 100 microseconds — and browsers add jitter on top so that repeated samples do not average out to a precise value. Date.now() has only millisecond resolution. SharedArrayBuffer, which allows a worker to spin on a counter at full speed and hand the main thread an arbitrarily precise interval, is gated behind cross-origin isolation, as is performance.measureUserAgentSpecificMemory().

The gate is the crossOriginIsolated flag, which requires Cross-Origin-Opener-Policy: same-origin and an embedder policy compatible with isolation — require-corp or credentialless — on the document, plus, under require-corp, cooperation from every embedded resource. Isolated documents get microsecond timers and shared memory back. The reasoning is that an isolated document has already committed to loading only cross-origin resources that opted in, so the process cannot be seeded with attacker-chosen bytes, and the high-resolution clock therefore cannot be pointed at a co-resident secret.

That reasoning is sound but conditional. Isolation is about what the document can load, not about what the document’s own origin contains. A page that renders user-supplied HTML on its own origin is isolated and injectable at the same time; the clock is precise and the attacker’s script is inside the trust boundary. Isolation narrows the Spectre surface, and it does nothing about XSS or about a compromised third-party script, which is the argument for the constraints in subresource integrity and third-party scripts.

The popup cost is real: same-origin COOP severs window.opener for cross-origin openers, which breaks authentication popups that rely on messaging back to the opener. The alternatives are full-page redirects, a popup that completes the exchange through a server-side redirect, or accepting same-origin-allow-popups and losing cross-origin isolation on that page.

Channels that survive the process boundary

Even with site isolation and blocked responses, a cross-origin page remains observable. The set of leaks that do not require reading memory is large enough to be a design constraint in its own right.

Presence and structure leak through frame counts and window.length, through the onload and onerror timing of embedded resources, through CSS selector matching against :visited (constrained heavily after history-sniffing was demonstrated), and through the mere fact that a navigation to a URL either commits or does not. Response size leaks through download progress and through the timing of the load event. Cache state leaks through the time a subresource takes to arrive, though the HTTP cache is now partitioned by top-level site, which removes the shared-cache probe that once made this reliable.

These channels are low-bandwidth and often require the victim to visit a page many times, which makes them practical for targeted attacks rather than mass exploitation. Their existence is why cross-origin isolation also disables the high-resolution clock and why browsers continue to reduce timer precision, and why a document that must not be probed should refuse framing and avoid embedding attacker-controlled URLs.

The honest summary is that the origin boundary is a policy enforced by code that runs next to the data it protects. Site isolation makes the “next to” part false for cross-site content, response blocking reduces what arrives, and timer coarsening makes the leftovers hard to measure. None of these make an origin trustworthy; they make the cost of crossing it high enough that other failures — an XSS, a permissive CORS configuration, a script from a CDN — become the cheaper path. The policy decisions that determine those failures are the subject of the earlier material in this cluster, beginning with how the same-origin policy computes origins.

References

  1. Spectre Attacks: Exploiting Speculative Execution — Kocher, Horn, Fogh, Genkin, Gruss, Haas, Hamburg, Lipp, Mangard, Prescher, Schwarz, Yarom — IEEE S&P 2019
  2. Cross-Origin Read Blocking for Web Developers — Chromium Project
  3. HTML Standard — Agent clusters — WHATWG