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.

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.

How COOP and COEP combine to unlock cross-origin isolation A diagram showing COOP severing the opener reference and COEP requiring resource opt-in, with both gating the crossOriginIsolated flag that enables SharedArrayBuffer and high-resolution timers. COOP: same-origin window.opener becomes null blocks XS-Leaks via opener COEP: require-corp every subresource must opt in via CORP or CORS crossOriginIsolated === true Unlocks: SharedArrayBuffer, Wasm threads, high-res timers, measureMemory()
COOP isolates the browsing-context group; COEP forces subresource opt-in. Only when both hold does the browser grant crossOriginIsolated and re-enable the powerful APIs disabled after Spectre.

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:

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;

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"

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>

Node/Express (Helmet)

const helmet = require('helmet');

app.use(
  helmet({
    crossOriginOpenerPolicy: { policy: 'same-origin' },
    crossOriginEmbedderPolicy: { policy: 'require-corp' },
    crossOriginResourcePolicy: { policy: 'cross-origin' },
  })
);

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)

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

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.