Cross-origin isolation: COOP, COEP, CORP
In 2018 Spectre demonstrated that a malicious page sharing a process with a sensitive
page could read its memory via timing side-channels. Browsers' answer: cross-origin
isolation. Three headers — COOP, COEP, CORP — work together to put the page in a
browsing-context-group of one, where Spectre-class attacks have nowhere to land.
Required for SharedArrayBuffer, high-resolution timers, and a few other
"powerful features." Mostly invisible until you need it.
The three headers
| Header | What it does |
|---|---|
| Cross-Origin-Opener-Policy (COOP) | "My window has no shared browsing context with windows it didn't open." Set on top-level documents. Value: same-origin. |
| Cross-Origin-Embedder-Policy (COEP) | "Every cross-origin resource I embed must opt in via CORP or CORS." Value: require-corp. |
| Cross-Origin-Resource-Policy (CORP) | "This resource agrees to be embedded by these origins." Set per-resource. Values: same-site, same-origin, cross-origin. |
COOP and COEP go on your top-level document. CORP goes on every static asset, image,
font, JS bundle. When all three line up, the browser flips
self.crossOriginIsolated === true and unlocks
SharedArrayBuffer, performance.now() at full resolution, and a
few other gated capabilities.
The threat model in plain terms
Without COOP, opening example.com via window.open gives the
opener a handle (window.opener) to your tab. Most apps null this out
defensively. With COOP same-origin, the browser severs that link
automatically. The cross-origin opener no longer has a usable handle to your window,
your COOP-protected tab no longer has a handle to its opener.
Without COEP, your origin can <img src="https://example.com/secret.png">
and use Spectre-style timing to extract pixel data. With COEP require-corp,
the image must explicitly opt in to being embedded (via CORP) — the resource owner
now controls who embeds it.
The third-party gotcha
Most CDNs serve assets without CORP headers (the default is "no opinion," which fails
COEP require-corp checks). Once you turn on COEP, every cross-origin
image, font, script, iframe that doesn't ship CORP breaks. fonts.googleapis.com
doesn't ship CORP. Your analytics provider's pixel doesn't. Stripe.js's iframe might.
Two escape hatches:
- credentialless mode (2023+).
Cross-Origin-Embedder-Policy: credentiallesspermits cross-origin resources but strips credentials (cookies, auth headers) from the request. The third-party resource sees an unauthenticated request. Works for fonts, public images. Fails for personalized embeds. - Proxy through your own origin. Self-host the third-party CSS/font/script. Slap CORP on it. Some teams already do this for performance; cross-origin isolation makes it a security requirement.
Deployment order
Don't try to do all three at once. The right order:
- CORP everywhere first. Add
Cross-Origin-Resource-Policy: same-originto all your static assets. No app behavior changes — same-origin requests don't care about CORP. Confirms your asset pipeline can set headers. - COOP next.
Cross-Origin-Opener-Policy: same-origin. Catcheswindow.openerdependencies. The mistake here: SSO popups thatpostMessageback to the opener. With COOP same-origin those won't work — switch tosame-origin-allow-popups. - COEP last.
Cross-Origin-Embedder-Policy: require-corporcredentialless. This is the one that breaks third-party embeds. Roll out behind a feature flag. Use Reporting API to collect the violations first.
Reporting API for COEP
Cross-Origin-Embedder-Policy-Report-Only: require-corp; report-to="coep-endpoint"
Reporting-Endpoints: coep-endpoint="https://example.com/coep-reports"
Browser sends a JSON report for every blocked sub-resource. After a week, your endpoint has a complete inventory of cross-origin resources without CORP. Self-host them or negotiate with the providers.
Who actually needs this
You need cross-origin isolation if you use:
SharedArrayBuffer(WASM threads, ffmpeg.wasm, photoshop.com)performance.measureUserAgentSpecificMemory()- High-precision
performance.now()(microsecond, not 100µs) JS Self-Profiling API
You should consider it as defense-in-depth even if you don't, especially if your app handles secrets in JS context (financial dashboards, password managers, identity providers).
You should NOT do it if your app is fundamentally an aggregator of cross-origin embeds — news sites with 30 ads from different networks, marketplace pages with seller-uploaded content from a different domain. The retrofit cost exceeds the security benefit.
The simplest correct deployment
# For a same-origin SPA serving static assets from the same domain:
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "credentialless" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
# Verify in the page:
<script>
if (self.crossOriginIsolated) console.log('COI active');
else console.warn('COI failed');
</script>
What we report
Our headers checker reports COOP, COEP, CORP presence. We don't currently
emit findings for absence — these are advanced posture markers, not basic hygiene. We
do flag inconsistent values (COEP set but CORP missing on serve assets) as INFO. The
scoring impact is small; this is informational for teams that already passed all the
basics.
Inventory your headers
Free Basic scan reports all security headers. Extended adds CSP maturity grading and report-uri reachability checks.
Run a scan