Web Platform · Web Security Model
Cross-Origin Isolation with COOP and COEP
How Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy combine to produce crossOriginIsolated documents, and what CORP must do on the other side of each load.
SharedArrayBuffer was withdrawn from browsers in 2018 and returned with a precondition. The object itself is not dangerous; what it enables is a high-resolution clock shared between workers, which turns a speculative execution side channel into a practical read primitive. The precondition for getting it back was that the document must be cross-origin isolated: it must prove that it cannot receive cross-origin data except from servers that explicitly opted in, and that it cannot share a browsing context group with an unrelated origin.
Two response headers establish that state. Cross-Origin-Opener-Policy controls which other documents may hold a reference to the page’s window. Cross-Origin-Embedder-Policy controls which cross-origin subresources the page may load. When both are set to their strictest values and every embedded resource cooperates, the document gains crossOriginIsolated === true.
What isolation unlocks
The observable difference is a set of APIs that are gated on the flag:
SharedArrayBuffercan be constructed and passed to workers, soAtomicscan be used for synchronisation across threads.performance.now()returns timestamps with a 5-microsecond resolution instead of the clamped value applied to non-isolated documents, which the specification puts at 100 microseconds and browsers further coarsen with jitter.performance.measureUserAgentSpecificMemory()becomes available, since precise memory attribution is another cross-origin leak.- WebAssembly threads and the
Atomics.waitblocking primitive on the main thread’s workers work as specified.
The clock is the point. With a coarse, jittered timer, a Spectre-style gadget can still be built but its signal is buried in noise; with microsecond resolution, repeated probes of cache state accumulate into a measurable difference. Isolation is therefore a trade: the page gives up the ability to embed arbitrary third-party resources without cooperation, and receives precise timing and shared memory in exchange.
COOP separates browsing context groups
A browsing context group is the set of windows and frames that can reach each other through window.opener, window.parent, and named targeting. Same-origin documents normally share one group. A page opened by a cross-origin page with window.open is in the opener’s group, which means the opener holds a WindowProxy to it and the opened page can walk back through window.opener.
Cross-Origin-Opener-Policy: same-origin breaks that. The document is placed in a new browsing context group, and any existing relationship with a cross-origin opener is severed: the opener’s reference to the new window becomes a closed proxy, and window.opener on the new document is null. Same-origin openers are unaffected, so a site that opens its own pages keeps working.
same-origin-allow-popups is the weaker variant. The document still isolates itself from a cross-origin opener, but popups it opens with window.open retain their opener relationship. This exists for the authentication pattern: a login page opens a provider’s popup, the popup posts a result back through window.opener, and the relying party needs that channel to stay open. Under plain same-origin, the popup would find window.opener null and the flow would hang.
The security value of COOP is that it removes a cross-origin reference into the isolated document. Without it, an attacker page could open the target, retain a WindowProxy, and use the properties that remain readable across origins — frame counts, closed, and the timing of navigation — as a leak channel into a document that is otherwise isolated. COOP also prevents the reverse: the isolated document cannot be navigated by a cross-origin opener that still holds a handle, closing a class of navigation-based attacks.
Origin-Agent-Cluster: ?1 is related but distinct. It requests that the agent cluster be keyed by origin rather than by site, so a.example.com and b.example.com do not share a JavaScript agent, and it disables the document.domain setter for the document. It is not required for crossOriginIsolated, but it strengthens the separation that COOP and COEP rely on, and it is cheap to add.
COEP makes embedding an explicit decision
Cross-Origin-Embedder-Policy: require-corp changes the default for every cross-origin subresource. Under it, a cross-origin image, script, stylesheet, font, or frame loads only if one of two conditions holds: the request was made in CORS mode and the response carries a valid Access-Control-Allow-Origin, or the response carries Cross-Origin-Resource-Policy with a value that permits the embedding document.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
The first two lines belong on the isolated document; the third is what a resource serves to control who may embed it. A same-origin resource needs no CORS, and with require-corp a same-origin load is permitted without further ado — the policy constrains cross-origin loads only. Everything from a different origin must opt in.
The reason for the requirement is that any cross-origin bytes in the process are potential Spectre input. If a page could load an attacker’s image or script without the attacker’s knowledge, the attacker’s data would be present in the isolated process, and the isolation guarantee would be meaningless. require-corp forces the resource owner to state that it accepts being embedded, which is why the header lives on the resource and not on the embedder.
credentialless is the softer alternative. It loads cross-origin no-cors subresources without credentials, so no Cross-Origin-Resource-Policy header is required from the host. The practical effect is that images and scripts from hosts that have never heard of COEP still load, at the cost of the resource seeing an anonymous request and of some cookie-dependent embeds breaking. Support and semantics here have shifted, so the value is best treated as a progressive enhancement rather than a guarantee.
Nested frames are the awkward case. An iframe under require-corp must itself be a CORS-enabled or CORP-permitted load, and the framed document must also send Cross-Origin-Embedder-Policy, or the frame is blocked. A third-party widget that cannot add COEP to its own responses cannot be embedded in an isolated page at all; the options are to proxy it through the parent’s origin, to move it into a non-isolated popup, or to drop it. A widget that has been moved out of the process entirely can still communicate with the page, subject to the message checks described in sandboxed frames and the postMessage trust boundary.
CORP from the resource owner’s side
Cross-Origin-Resource-Policy takes three values that describe who may embed the response: same-origin (only the resource’s own origin), same-site (the same registrable domain, which includes sibling subdomains), and cross-origin (anyone). The header is checked by the browser on the embedding side, using the response’s value and the embedder’s origin.
CORP and CORS answer different questions and are not interchangeable. CORS says “this origin may read this response with script”; CORP says “this origin may include this response in a document at all”. A resource without CORS can still be embedded as an image if it sends Cross-Origin-Resource-Policy: cross-origin. A resource with permissive CORS passes COEP without any CORP header, because the CORS check is the opt-in.
The header is also the mechanism that stops the classic no-CORS embedding attacks — a cross-origin image or script that leaks through size, timing, or error events. Setting same-origin on responses that are never meant to be embedded is a low-cost hardening step independent of whether the site uses COEP, and it is checked in every context, not just isolated ones. The CSP directive frame-ancestors covers only frame embedding, so CORP is the broader control for images, fonts, and scripts.
Deploying without breaking the sign-in popup
The order that fails least often is COOP first, then COEP in report-only mode, then CORP on owned resources, then enforcement.
COOP same-origin breaks any flow that expects window.opener to survive across origins, and OAuth and payment popups are the usual casualties. The fix is same-origin-allow-popups on the page that opens the popup, which keeps the popup channel while still isolating from a cross-origin opener. That value does not by itself produce crossOriginIsolated; a document that needs shared memory and also opens cross-origin popups has to restructure the flow, typically by using a full-page redirect or by completing the exchange in a top-level window the opener polls through postMessage after navigation.
COEP require-corp breaks every embed whose host has not added CORP or CORS, and it breaks them silently from the page’s perspective: the element fires an error and the console names the blocked URL, but nothing loads. Cross-Origin-Embedder-Policy-Report-Only produces reports without blocking, which is the only practical way to inventory a page’s embeds. Such an inventory covers whatever the page actually loads — analytics beacons, video, payment frames, fonts from a CDN — and each item needs either a header added at the source or a local proxy.
Fonts deserve their own note: cross-origin @font-face loads are already CORS-mode requests, so a font server that sends Access-Control-Allow-Origin passes COEP without CORP. The same is true for fetch and module scripts, which are always CORS-mode. The embeds that need CORP are the legacy no-cors ones: images, media, classic scripts without crossorigin, stylesheets, and iframes.
Finally, the isolation must hold for the whole frame tree the document can script. The embedder policy check runs for every child navigable, so each frame must carry its own compatible Cross-Origin-Embedder-Policy, same-origin or not; a parent whose policy is compatible with isolation blocks a frame that sends none. A single non-cooperating frame does not silently degrade the isolation — it fails to load — which is why the report-only phase tends to surface a longer list than expected. The process-level details of why the isolation is needed are treated in site isolation and the limits of the origin boundary.