COOP + COEP for Spectre & crossOriginIsolated
This page is a focused procedure within the Cross-Origin Isolation: COOP, COEP & CORP reference. Spectre proved that any secret sharing a renderer process with attacker JavaScript can be read through a timing side channel. Browsers responded by disabling SharedArrayBuffer and coarsening timers; they hand those capabilities back only to documents that prove they are cross-origin isolated. That proof requires two headers acting together — Cross-Origin-Opener-Policy set to same-origin and Cross-Origin-Embedder-Policy set to require-corp — after which self.crossOriginIsolated evaluates to true. This page gives the exact values, per-platform configuration, and the verification that the flag actually flipped.
The Spectre threat model in one diagram
Spectre is a speculative-execution attack: the CPU speculatively executes code past a mispredicted branch, touches memory it should not, and although the architectural result is discarded, the microarchitectural cache state is not. Attacker JavaScript running in the same address space as a victim’s secrets — cookies, another origin’s response body pulled in by a stray load — measures cache timing to reconstruct those bytes. The two ingredients the attacker needs are a high-resolution clock to distinguish a cache hit from a miss, and ideally shared memory to build a fast timer. Both were freely available on the web before 2018, which is why SharedArrayBuffer and microsecond-precision performance.now() were withdrawn.
Cross-origin isolation removes the co-residency premise rather than the clock. When a document is isolated, the browser guarantees no cross-origin document shares its browsing-context group and every embedded resource consented to being loaded, so there is no unconsented cross-origin secret in the process to steal — and only then does it return the powerful timers and shared memory.
Configuration Syntax & Exact Values
Two response headers on the top-level document, verbatim, no quotes and no extra tokens:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin— puts the document in a fresh browsing-context group, setswindow.openertonull, and severs any window it opens. This is the value that satisfies isolation;same-origin-allow-popupsandunsafe-nonedo not.Cross-Origin-Embedder-Policy: require-corp— refuses every cross-origin subresource that does not opt in with aCross-Origin-Resource-Policyheader or a passing CORS handshake.credentiallessis the milder alternative that loadsno-corscross-origin resources without credentials and also satisfies isolation, at the cost of dropping cookies on those loads.
Both must hold for the same document at the same time. Either header alone leaves self.crossOriginIsolated at false and SharedArrayBuffer undefined in the constructor. Every genuinely cross-origin asset the isolated page pulls in must additionally send, at its own origin:
Cross-Origin-Resource-Policy: cross-origin
First-party assets may use Cross-Origin-Resource-Policy: same-origin — narrower is safer, and a same-origin CORP still passes a require-corp page because the resource and document share an origin.
Server-Side Configuration
Nginx
location / {
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
}
# First-party assets the isolated page embeds
location ~* \.(js|mjs|wasm|woff2|png|jpg|css)$ {
add_header Cross-Origin-Resource-Policy "same-origin" always;
}
The always flag emits the headers on error responses too (4xx/5xx). Without it, navigating to a 404 returns a document with no COOP/COEP, which silently drops isolation for that view. See the full Nginx security headers configuration for add_header inheritance rules — a nested location with its own add_header discards the parent’s headers entirely.
Apache (mod_headers)
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
<FilesMatch "\.(js|mjs|wasm|woff2|png|jpg|css)$">
Header always set Cross-Origin-Resource-Policy "same-origin"
</FilesMatch>
Header always set writes the header onto internally generated error responses as well; a plain Header set only fires on 2xx and 3xx. More detail lives in Apache .htaccess & VirtualHost hardening.
Node.js / Express (Helmet)
const helmet = require("helmet");
app.use(helmet.crossOriginOpenerPolicy({ policy: "same-origin" }));
app.use(helmet.crossOriginEmbedderPolicy({ policy: "require-corp" }));
// CORP on first-party static assets
app.use(helmet.crossOriginResourcePolicy({ policy: "same-origin" }));
Helmet writes each header on every response it processes, including error middleware output, so no always equivalent is needed. Full setup is in Node.js, Express & Helmet configuration.
Django / FastAPI
# Django middleware
class IsolationHeadersMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["Cross-Origin-Opener-Policy"] = "same-origin"
response["Cross-Origin-Embedder-Policy"] = "require-corp"
return response
Setting the header on the response object applies it to every status code the view returns, which is the framework equivalent of always. See FastAPI & Django security middleware.
Cloudflare (Transform Rules)
// Response Header Transform Rule expression: (http.host eq "app.example.com")
// Static rule — set two headers on the document response:
// Cross-Origin-Opener-Policy = same-origin
// Cross-Origin-Embedder-Policy = require-corp
Cloudflare Transform Rules run at the edge on every eligible response, including origin errors, so isolation holds even when the origin is down. Configuration steps are in Cloudflare page rules & headers.
Next.js / Vercel
// next.config.js
module.exports = {
async headers() {
return [{
source: "/:path*",
headers: [
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
],
}];
},
};
The headers() function attaches to all matched routes regardless of status; there is no separate always flag. See Vercel & Next.js header management.
Diagnostic & Verification Steps
Confirm the headers reach the wire first, then confirm the browser acted on them.
Check the response headers with curl:
curl -sI https://app.example.com/ | grep -i 'cross-origin'
Expected output:
cross-origin-opener-policy: same-origin
cross-origin-embedder-policy: require-corp
If either line is missing, the browser will never set the flag. The authoritative test is inside the document — headers on the wire are necessary but not sufficient, because a single blocked COEP subresource does not stop isolation, but a wrong COOP value or a mixed-content downgrade will. Run this in the DevTools console on the loaded page:
console.log(self.crossOriginIsolated);
// true -> isolation is active, SharedArrayBuffer is defined
// false -> at least one header is missing, wrong, or overridden
When it prints true, the constructor is available:
console.log(typeof SharedArrayBuffer); // "function"
new SharedArrayBuffer(1024); // no throw
To see why isolation failed, DevTools reports it directly. Open Application → Frames → top, and the panel lists “Cross-Origin Isolated: Yes/No” alongside the effective COOP and COEP values the browser parsed. Blocked subresources surface in the Network tab and in the console:
GET https://cdn.example.com/lib.js net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep
That error names the exact cause: a cross-origin resource under a require-corp page that sent no CORP header. For a systematic walk-through of these failures, see Debugging Resources Blocked by COEP.
Edge Cases, Security Implications & Safe Rollback
Trap 1 — third-party embeds and popups break under COEP. The moment require-corp is enforced, every cross-origin <iframe>, script, image, font, and analytics beacon that does not send CORP or pass CORS is blocked. Ad tags, chat widgets, map embeds, and social buttons are the usual casualties. Enforcing COEP is the risky half; enforce it under Cross-Origin-Embedder-Policy-Report-Only first and collect the blocked-resource reports before flipping to live. Note that report-only COEP never grants isolation — it only tells you what would break.
Trap 2 — OAuth and payment popups need the opener. COOP: same-origin sets window.opener to null in the popup, which breaks the redirect-back-to-opener pattern many OAuth and 3-D Secure flows use. If you must keep such a popup, you cannot reach isolation on that document with plain same-origin; you either move the popup flow to a non-isolated route, or switch that flow to a redirect (top-level navigation) instead of a popup. There is no COOP value that both isolates and preserves a cross-origin opener.
Trap 3 — credentialless silently strips cookies. Switching COEP to credentialless unblocks public CDN assets without CORP, but any cross-origin resource that relied on a cookie (a signed CDN URL is fine; a cookie-gated private image is not) now loads anonymously and may 401 or return the wrong variant. It is the right choice for pages that embed only public third-party assets, the wrong one for pages embedding authenticated cross-origin content.
Rollback. COOP and COEP are not persisted like HSTS — there is no max-age, no cached state in the browser. Removing the headers from the server takes effect on the very next navigation; there is nothing to expire and no pin to clear. To roll back, delete the two add_header/Header set/config lines and reload. This makes staged rollout cheap: a bad enforcement can be reverted in one deploy with no lingering client state, unlike a transport pin.
Conclusion
Prove the pair in staging with curl and a self.crossOriginIsolated check, then enable Cross-Origin-Embedder-Policy-Report-Only in production to collect blocked-resource reports without breaking anything. Fix or CORP-annotate every reported asset, switch COEP to enforcing require-corp alongside COOP: same-origin, and confirm the flag flips to true on real traffic. Because neither header is cached, the whole sequence is reversible in a single deploy.
Frequently Asked Questions
Why does self.crossOriginIsolated stay false when both headers are on the wire?
The most common causes are a report-only COEP header (which never isolates), a same-origin-allow-popups COOP value instead of same-origin, or the document not being a secure context. Check the DevTools Application → Frames → top panel, which shows the exact COOP and COEP values the browser parsed and whether the frame is isolated.
Do blocked COEP subresources prevent isolation?
No. Once COOP: same-origin and enforcing COEP: require-corp are set on the document, crossOriginIsolated is true even if some cross-origin subresources are blocked. Those resources simply fail to load; the flag reflects the document’s policy, not whether every embed succeeded.
Is credentialless enough for isolation, or do I still need CORP everywhere?
Cross-Origin-Embedder-Policy: credentialless satisfies isolation on its own — you do not need CORP on cross-origin resources. The trade-off is that those resources load without cookies or credentials, so any cookie-gated cross-origin asset must move to a CORS-plus-CORP model or be served publicly.
Does enabling isolation require HTTPS?
Yes. SharedArrayBuffer and the other gated APIs are exposed only in secure contexts, and a mixed-content subresource on an otherwise-isolated HTTPS page breaks the secure-context requirement. Serve the document and its assets over HTTPS with no mixed content.
How do I roll back if COEP blocks critical third-party embeds?
Remove the two header lines from your server config and redeploy. COOP and COEP carry no max-age and are not cached, so the change takes effect on the next navigation with no client-side state to clear — a deliberately safer rollback than a transport pin.