Cross-Origin Isolation: COOP, COEP & CORP
This guide is part of the Web Security Headers Fundamentals reference. It covers the three response headers that together establish a cross-origin isolated browsing context: Cross-Origin-Opener-Policy (COOP), Cross-Origin-Embedder-Policy (COEP), and Cross-Origin-Resource-Policy (CORP). These headers are not interchangeable with the framing or transport controls — they exist to defend the process boundary itself against speculative-execution side channels and cross-site information leaks, and to re-unlock powerful APIs that browsers disabled in their wake.
Threat Model & Protocol Mechanics
In January 2018 the Spectre class of speculative-execution vulnerabilities demonstrated that any data resident in a renderer process’s address space could potentially be read by attacker-controlled JavaScript through timing side channels. The browser’s process model was the only hard boundary, and the high-resolution timers and shared-memory primitives that web applications relied on — SharedArrayBuffer, performance.now() at microsecond granularity, WebAssembly threads — were precisely the tools an attacker needed to build a reliable side-channel exploit. Browser vendors responded by disabling SharedArrayBuffer outright and coarsening timer resolution.
Cross-origin isolation is the contract that earns those capabilities back. A document becomes cross-origin isolated only when it proves two things to the browser: that no cross-origin document shares its browsing-context group (so a malicious opener cannot script it or read its memory through a shared event loop), and that every resource it loads has explicitly opted in to being embedded by it. The first guarantee comes from COOP, the second from COEP, and the per-resource opt-in that COEP demands is expressed by CORP. When both COOP and COEP are satisfied, self.crossOriginIsolated evaluates to true and the gated APIs return.
The second, broader threat class these headers address is XS-Leaks (cross-site leaks): an attacker page that holds a reference to your window — via window.open() or by being your opener — can probe window.frames.length, window.length, navigation timing, or postMessage behavior to infer whether you are logged in, which account is active, or whether a search returned results. COOP severs that window reference entirely, which is its value independent of SharedArrayBuffer.
How COOP severs the opener reference
Cross-Origin-Opener-Policy controls whether a document keeps a scripting relationship with the page that opened it (or that it opens). Under the default unsafe-none, window.opener remains a live cross-origin proxy. Under same-origin, when a document with that policy is reached, the browser places it in a new browsing-context group: window.opener becomes null, and any window it opens cannot reach back. The opener and the openee can no longer observe each other’s frame counts, trigger navigations, or exchange messages. This is enforced by the browser at navigation time, per the HTML standard’s COOP processing model — it is not cached like HSTS max-age; the header must accompany every navigation.
same-origin-allow-popups is the relaxed variant: the document keeps the ability to open popups that retain an opener reference (needed for OAuth and payment popups), while still isolating itself from any opener that reached it. It is not sufficient for cross-origin isolation — only same-origin is.
How COEP enforces embedding consent
Cross-Origin-Embedder-Policy flips the default loading contract. Normally a document can load any cross-origin subresource (images, scripts, fonts) without that resource agreeing. Under require-corp, the browser refuses to load any cross-origin subresource unless it explicitly grants permission, either with a Cross-Origin-Resource-Policy header that permits the embedding origin or with a successful CORS handshake (crossorigin attribute plus Access-Control-Allow-Origin). A resource that grants neither is blocked.
credentialless is the pragmatic alternative introduced to ease adoption: instead of demanding CORP/CORS from every third-party resource, the browser loads cross-origin no-cors subresources without credentials (no cookies, no client certs). The resource still need not send CORP, but it is fetched anonymously. This unblocks public CDN assets that never opted in, at the cost of any cookie-gated cross-origin resource.
How CORP marks a resource embeddable
Cross-Origin-Resource-Policy is the per-resource counterpart. It is set on the resource (the image, the font, the script), not the document, and declares who may embed it: same-origin (only the exact origin), same-site (any subdomain of the registrable domain, scheme-sensitive), or cross-origin (anyone). Under a COEP require-corp page, a cross-origin subresource must carry Cross-Origin-Resource-Policy: cross-origin (or pass CORS) or it is blocked. CORP is defined in the Fetch standard and also serves as a standalone defense against side-channel resource probing even when you are not pursuing full isolation.
Directive Syntax & Spec
Each header carries a single value (COEP and CORP) or a value plus optional report-to parameter (COOP/COEP). Unknown tokens are treated as the safe default, so a typo silently leaves you unprotected.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: cross-origin
Cross-Origin-Opener-Policy
| Directive | Type | Default | Security Impact | When to Deviate |
|---|---|---|---|---|
unsafe-none |
COOP token | yes (default) | No isolation; window.opener stays live, openers can script and probe the document. |
Never deliberately; this is the unprotected baseline. |
same-origin-allow-popups |
COOP token | no | Isolates from any opener that reached this page, but popups it opens keep their opener. Does not enable cross-origin isolation. | OAuth/payment popup flows where the page itself must open authenticated popups. |
same-origin |
COOP token | no | Full opener severance; window.opener is null. Required half of cross-origin isolation. |
Any page pursuing crossOriginIsolated or hardening against opener-based XS-Leaks. |
Cross-Origin-Embedder-Policy
| Directive | Type | Default | Security Impact | When to Deviate |
|---|---|---|---|---|
unsafe-none |
COEP token | yes (default) | Subresources load with no opt-in requirement. No isolation. | Default for pages not pursuing isolation. |
require-corp |
COEP token | no | Every cross-origin subresource must send a permitting CORP header or pass CORS, or it is blocked. Strictest. | Full cross-origin isolation where you control or can configure every subresource. |
credentialless |
COEP token | no | Cross-origin no-cors subresources load without credentials instead of requiring CORP. Eases third-party adoption. |
Pages with public CDN assets you cannot add CORP to; cookie-gated cross-origin resources will break. |
Cross-Origin-Resource-Policy
| Directive | Type | Default | Security Impact | When to Deviate |
|---|---|---|---|---|
same-origin |
CORP token | no (absence ≈ permissive for no-cors loads) | Resource embeddable only by the identical scheme + host + port. Tightest anti-leak posture. | Private API responses, authenticated images that must never be embedded cross-site. |
same-site |
CORP token | no | Embeddable by any host on the same registrable domain; scheme-sensitive (downgrade from https blocked). | Assets shared across app.example.com and cdn.example.com. |
cross-origin |
CORP token | no | Any origin may embed the resource. Required for public assets consumed by COEP require-corp pages. |
CDN-hosted fonts, images, scripts intended for cross-origin embedding. |
Malformed-syntax gotchas:
- COEP and COOP take no quotes and no source lists —
Cross-Origin-Opener-Policy: "same-origin"is invalid and falls back tounsafe-none. same-origin-allow-popupsdoes not satisfy cross-origin isolation; a common mistake is setting it for an OAuth flow and then wondering whySharedArrayBufferis still undefined.- CORP
same-sitetreats scheme as significant: anhttp://resource will not be embeddable by anhttps://same-site page. - Setting COEP
require-corpwithout auditing subresources is the single most common cause of a broken deploy — every cross-origin image, font, and script must opt in. See debugging COEP-blocked resources. - These headers are orthogonal to Content-Security-Policy and X-Frame-Options: CSP
frame-ancestorsgoverns who may frame you; CORP governs who may embed your subresources; they do not substitute for one another.
Platform-Specific Implementation
To make a top-level document cross-origin isolated, set COOP and COEP on that document’s response, and set CORP on every resource you serve that other isolated pages embed. Apply on all status codes so error pages do not silently drop isolation.
Nginx
# On the top-level document
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# On every resource that cross-origin isolated pages embed
add_header Cross-Origin-Resource-Policy "cross-origin" always;
- Security impact:
alwaysemits the headers on 4xx/5xx too — without it Nginx dropsadd_headeron error pages, and an isolated app navigating to an error response would losecrossOriginIsolated. - Verification:
curl -sI https://your-domain.com | grep -iE 'cross-origin-(opener|embedder|resource)-policy'
Apache (mod_headers)
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Resource-Policy "cross-origin"
- Security impact:
alwaysapplies across all HTTP status codes; the defaultonsuccesstable skips errors. Requiresmod_headersloaded. - Verification:
apachectl -M 2>/dev/null | grep headers_module && curl -sI https://your-domain.com | grep -i cross-origin
IIS (web.config)
<configuration>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Cross-Origin-Opener-Policy" value="same-origin" />
<add name="Cross-Origin-Embedder-Policy" value="require-corp" />
<add name="Cross-Origin-Resource-Policy" value="cross-origin" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
- Security impact:
customHeadersinjects at the site/application level before ASP.NET processing, covering static and dynamic responses uniformly. Scope CORP narrowly — placingcross-originsite-wide can over-expose private endpoints. - Verification:
(Invoke-WebRequest -Uri https://your-domain.com).Headers['Cross-Origin-Opener-Policy']
Node/Express (Helmet)
const helmet = require('helmet');
app.use(
helmet({
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginEmbedderPolicy: { policy: 'require-corp' },
crossOriginResourcePolicy: { policy: 'cross-origin' },
})
);
- Security impact: Middleware runs before route handlers and emits the headers on every response Helmet touches. Mount it before your routers so error responses inherit isolation. Helmet 5+ ships
crossOriginEmbedderPolicyenabled — confirm subresources opt in before upgrading. - Verification:
curl -sI http://localhost:3000 | grep -iE 'cross-origin-(opener|embedder|resource)-policy'
Cloudflare (Transform Rules)
Rules → Transform Rules → Modify Response Header → Set static:
Cross-Origin-Opener-Policy = same-origin
Cross-Origin-Embedder-Policy = require-corp
Cross-Origin-Resource-Policy = cross-origin (scope to asset paths)
- Security impact: Edge enforcement applies before traffic reaches the origin and covers cache hits. Use Set (not Add) to avoid stacking a duplicate header. Scope the CORP rule to your static-asset paths rather than the whole zone so you do not over-expose API responses.
- Verification:
curl -sI https://your-domain.com | grep -iE 'cross-origin-(opener|embedder|resource)-policy|cf-cache-status'
Verification & Diagnostic Workflows
Confirm the headers are present on the document, then confirm the runtime flag flipped.
# Document-level headers (COOP + COEP)
curl -sI https://your-domain.com | grep -iE 'cross-origin-(opener|embedder)-policy'
# Expected:
# cross-origin-opener-policy: same-origin
# cross-origin-embedder-policy: require-corp
# Resource-level header (CORP) on an embedded asset
curl -sI https://cdn.your-domain.com/app.js | grep -i cross-origin-resource-policy
# Expected:
# cross-origin-resource-policy: cross-origin
Runtime flag. The authoritative check runs in the browser DevTools Console on the loaded page:
self.crossOriginIsolated // true only when COOP same-origin AND COEP require-corp both hold
If it returns false with both headers set, a subresource is being blocked and COEP has refused isolation. Open DevTools → Network, look for requests with a red status and a (blocked:NotSameOriginAfterDefaultedToSameOriginByCoep) reason, and check Application → Frames → top → Security & isolation which reports the computed COOP/COEP status and why isolation was denied.
CI/CD gate. Fail the pipeline if either document header is missing:
hdrs=$(curl -sI https://your-domain.com)
echo "$hdrs" | grep -qi 'cross-origin-opener-policy: same-origin' \
&& echo "$hdrs" | grep -qi 'cross-origin-embedder-policy: require-corp' \
|| { echo "FAIL: cross-origin isolation headers incomplete"; exit 1; }
Troubleshooting, Misconfigurations & Safe Rollback
- Symptom:
self.crossOriginIsolatedisfalsedespite COOP+COEP set. → A cross-origin subresource lacks CORP/CORS and COEP denied isolation. Find it in the Network panel and addCross-Origin-Resource-Policy: cross-originto it, or switch COEP tocredentialless. Walkthrough in debugging COEP-blocked resources. - Symptom: third-party ads, analytics, or embedded iframes stopped loading after enabling COEP. → Those resources do not send CORP and cannot pass CORS. Switch COEP to
credentialless(loads them without cookies) or remove them from the isolated context. - Symptom: OAuth or payment popup broke after setting COOP
same-origin. →same-originnullswindow.opener, breaking the popup’s callback to the parent. Usesame-origin-allow-popupsif you need the popup flow and do not requireSharedArrayBuffer; the two requirements are mutually exclusive on the same document. - Symptom: subresource works in isolation but breaks under a COEP page elsewhere. → The asset is missing CORP. Add
Cross-Origin-Resource-Policy: cross-origin(orsame-site) at the asset’s origin. See enabling cross-origin isolation for SharedArrayBuffer. - Symptom: duplicate Cross-Origin- header.* → Origin and CDN both set it. Pick one layer; on Cloudflare use Set, not Add.
- Safe rollback: isolation is not a security floor the way framing or transport headers are — if COEP breaks production loading and you cannot immediately fix every subresource, remove COEP only while keeping COOP:
Header unset Cross-Origin-Embedder-Policy
Header always set Cross-Origin-Opener-Policy "same-origin"
Removing COEP disables SharedArrayBuffer but preserves the COOP opener-severance defense against XS-Leaks. Prefer switching to credentialless before removing COEP entirely.
Frequently Asked Questions
Do I need all three headers?
For full cross-origin isolation you need COOP same-origin and COEP require-corp on the document; CORP cross-origin is set on the subresources an isolated page embeds, not on the document. If you only want to harden against opener-based cross-site leaks, COOP alone is valuable. CORP is also useful standalone to stop other sites embedding your private resources.
Why is SharedArrayBuffer still undefined after I set COOP and COEP?
Either a subresource is blocked (so crossOriginIsolated is false), or you set COOP to same-origin-allow-popups instead of same-origin. Only same-origin satisfies isolation. Check self.crossOriginIsolated in the console and the Network panel for COEP-blocked requests.
What is the difference between require-corp and credentialless?
require-corp blocks any cross-origin subresource that does not explicitly opt in via CORP or CORS. credentialless instead loads cross-origin no-cors subresources without credentials (no cookies), so public assets work without changes — at the cost of breaking any cookie-gated cross-origin resource. Both grant crossOriginIsolated.
Does COOP replace X-Frame-Options or CSP frame-ancestors? No. COOP governs the relationship with a window’s opener and openee; X-Frame-Options and frame-ancestors govern who may frame your page. They defend different surfaces and should both be deployed.
Will enabling these headers slow down my site? The headers themselves are negligible. The operational cost is auditing and adding CORP to every cross-origin subresource; once configured there is no runtime penalty, and isolation can actually improve security-sensitive timing APIs you rely on.