Enabling Cross-Origin Isolation for SharedArrayBuffer
This page is a focused procedure under the Cross-Origin Isolation: COOP, COEP & CORP reference. SharedArrayBuffer, WebAssembly threads, performance.measureMemory(), and unclamped high-resolution timers are all gated behind the crossOriginIsolated flag. That flag flips to true only when the top-level document sends the exact pair of headers below and every cross-origin subresource opts in. This is the precise configuration to get there.
Configuration Syntax & Exact Values
Two headers on the top-level document, with no quotes and no extra tokens:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin— places the document in its own browsing-context group and setswindow.openertonull. Onlysame-originqualifies;same-origin-allow-popupsdoes not enable isolation.Cross-Origin-Embedder-Policy: require-corp— refuses to load any cross-origin subresource unless it sends a permittingCross-Origin-Resource-Policyheader or passes a CORS handshake.
Every cross-origin asset the isolated page loads must additionally carry, at its own origin:
Cross-Origin-Resource-Policy: cross-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|wasm|woff2|png|jpg)$ {
add_header Cross-Origin-Resource-Policy "same-origin" always;
}
The always flag ensures the headers survive on error responses so navigating to a 404 does not silently drop isolation. First-party assets can use same-origin for CORP; only genuinely cross-origin assets need cross-origin.
Apache (mod_headers)
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
<FilesMatch "\.(js|wasm|woff2|png|jpg)$">
Header always set Cross-Origin-Resource-Policy "same-origin"
</FilesMatch>
always applies the headers across all status codes; the default onsuccess table would skip errors. Requires mod_headers.
Cloudflare (Transform Rules)
Rules → Transform Rules → Modify Response Header → Set static:
When URI Path equals /app (the isolated document)
Cross-Origin-Opener-Policy = same-origin
Cross-Origin-Embedder-Policy = require-corp
When URI Path matches static assets
Cross-Origin-Resource-Policy = cross-origin
Use Set, not Add, so the edge does not stack a duplicate header on an origin-set one.
Node/Express (Helmet)
const helmet = require('helmet');
app.use(
helmet({
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginEmbedderPolicy: { policy: 'require-corp' },
crossOriginResourcePolicy: { policy: 'same-origin' },
})
);
Mount the middleware before your routers so every response — including errors — carries the headers.
Preview Breakage With Report-Only Headers
Enforcing require-corp cold on a page that embeds dozens of third-party assets is the fastest way to a broken production route. Both policies ship a report-only twin that computes the same decisions and surfaces every violation without actually blocking anything:
Cross-Origin-Opener-Policy-Report-Only: same-origin
Cross-Origin-Embedder-Policy-Report-Only: require-corp
Reporting-Endpoints: coop-coep="https://your-domain.com/reports"
While only the report-only headers are set, self.crossOriginIsolated stays false and SharedArrayBuffer stays undefined — report-only never grants isolation, it only measures. What it does give you is a full inventory of the subresources that would be blocked once you enforce, delivered two ways: as POST bodies to the Reporting-Endpoints URL, and as warnings in DevTools → Network (each would-be-blocked request is flagged even though it still loaded). Run the enforcing pair and the report-only pair together during migration — the enforced page can carry the report-only variant of a stricter policy you plan to adopt next, so require-corp enforced alongside credentialless-report-only tells you whether tightening further is safe. Once the report queue is empty for a full traffic cycle, swap the report-only headers for their enforcing form.
Diagnostic & Verification Steps
Confirm the document headers, then confirm the runtime flag.
curl -sI https://your-domain.com/app | grep -iE 'cross-origin-(opener|embedder)-policy'
# Expected:
# cross-origin-opener-policy: same-origin
# cross-origin-embedder-policy: require-corp
When a subresource is the suspect, probe it directly at its own origin rather than guessing from the page. A cross-origin asset must answer with a CORP header that permits your embedding:
curl -sI https://cdn.example.com/lib.js | grep -i 'cross-origin-resource-policy'
# Expected for a third-party asset:
# cross-origin-resource-policy: cross-origin
# A missing line here is why require-corp blocked it.
An empty result means the asset sends no CORP at all — under require-corp that is a hard block, and your options are to ask the provider to add the header, proxy the asset through your own origin (where you can attach CORP), or move to credentialless. Then confirm the runtime flag in the browser DevTools Console on the loaded page:
self.crossOriginIsolated
// Expected: true
typeof SharedArrayBuffer
// Expected: "function"
If self.crossOriginIsolated is true, SharedArrayBuffer is constructable and Wasm threads will instantiate. If it is false, open DevTools → Network and look for requests blocked with reason (blocked:NotSameOriginAfterDefaultedToSameOriginByCoep) — those are subresources missing CORP. The Application → Frames → top → Security & isolation panel reports the computed COOP/COEP state and the reason isolation was denied.
Work the failure top-down. The flag is a single boolean gated by an ordered chain — a malformed COOP value masks a missing CORP downstream, so fix the headers in the order the browser evaluates them rather than guessing.
Service Workers can quietly break isolation. A Service Worker that controls the isolated page must serve the document response with the COOP/COEP pair intact, and any cross-origin response it returns from its cache must still carry a permitting CORP header. A worker that replays a cached third-party response stripped of CORP reintroduces exactly the block that require-corp forbids, so self.crossOriginIsolated flips to false on the controlled load even though a direct network fetch of the same page isolates fine. Test with the worker active, not just on first, uncontrolled load.
Edge Cases, Security Implications & Safe Rollback
- Third-party resources need CORP or CORS. Under
require-corp, every cross-origin image, font, script, or iframe must sendCross-Origin-Resource-Policy: cross-origin(or pass CORS with thecrossoriginattribute). Assets on a CDN you do not control, and that do not send CORP, will be blocked and isolation will fail. Audit the Network panel before shipping. The full debugging procedure is in debugging COEP-blocked resources. - The credentialless alternative. If you cannot add CORP to third-party assets, switch to
Cross-Origin-Embedder-Policy: credentialless. It still grantscrossOriginIsolatedbut loads cross-originno-corssubresources without credentials (no cookies). Any cross-origin resource that depends on a cookie — an authenticated image or a personalized widget — will break undercredentialless, so it is a trade, not a free pass.
- Popup auth flows break under COOP
same-origin. Becausesame-originsetswindow.openertonull, an OAuth or payment popup that calls back towindow.opener.postMessage(...)will fail.same-origin-allow-popupswould preserve the popup but does not grant isolation — you cannot have bothSharedArrayBufferand an opener-coupled popup on the same document. Move the popup flow to a non-isolated page, or switch to a redirect-based auth flow. - Iframes inherit a requirement. A cross-origin iframe inside an isolated page must itself send COEP
require-corp(orcredentialless) and a permitting CORP, or it is blocked. Embedding third-party iframes in an isolated context usually means coordinating with that provider. document.domainis disabled. In acrossOriginIsolatedcontext, assigning todocument.domainbecomes a silent no-op — the browser refuses the legacy same-site relaxation because it would undermine the isolation guarantee. Any code that still leans ondocument.domainto reach a sibling subdomain frame must be rewritten aroundpostMessagebefore the page can be isolated, or those cross-frame calls will start throwing.
These headers do not delete data or lock you out the way HSTS preload does, so rollback is low-risk: remove COEP only while keeping COOP same-origin to retain the opener-severance defense:
Header unset Cross-Origin-Embedder-Policy
Header always set Cross-Origin-Opener-Policy "same-origin"
Removing COEP disables SharedArrayBuffer but leaves your XS-Leak hardening intact. Prefer switching to credentialless before a full removal.
Frequently Asked Questions
Is COOP same-origin-allow-popups enough for SharedArrayBuffer?
No. Only same-origin enables cross-origin isolation. same-origin-allow-popups preserves opener-coupled popups but leaves self.crossOriginIsolated false, so SharedArrayBuffer stays undefined.
Do first-party scripts on the same origin need CORP?
Same-origin subresources are not blocked by COEP require-corp, so strictly they do not need CORP to load. Setting Cross-Origin-Resource-Policy: same-origin on them is still good practice to stop other sites embedding them.
My Wasm module fails to instantiate threads — why?
Wasm threads require SharedArrayBuffer, which requires crossOriginIsolated === true. Check that flag first; if it is false, a subresource is blocked or COOP is not exactly same-origin.
Can I enable isolation only on one route?
Yes. COOP and COEP are per-document. Apply them only to the route that needs SharedArrayBuffer (scope the Nginx location or Cloudflare path match), leaving the rest of the site non-isolated so popup and third-party flows elsewhere keep working.
Conclusion
Roll out incrementally: set COOP and COEP on a staging copy of the target route, open it and confirm self.crossOriginIsolated === true with no blocked subresources in the Network panel, then promote to production. If third-party assets block isolation and you cannot add CORP, fall back to credentialless rather than abandoning isolation. Keep COOP same-origin even if you drop COEP, so the opener-severance defense survives.