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

Every cross-origin asset the isolated page loads must additionally carry, at its own origin:

Cross-Origin-Resource-Policy: cross-origin
The gate from COOP and COEP to SharedArrayBuffer A left-to-right flow showing COOP same-origin and COEP require-corp combining into crossOriginIsolated true, which enables SharedArrayBuffer and Wasm threads. COOP same-origin COEP require-corp crossOriginIsolated === true SharedArrayBuffer Wasm threads enabled
Both headers must hold simultaneously; either one missing leaves crossOriginIsolated false and SharedArrayBuffer undefined.

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.

Decision tree for a false crossOriginIsolated flag An ordered troubleshooting flow that checks the COOP value, then COEP presence, then blocked subresources, then the embedding frame. crossOriginIsolated === false COOP is exactly same-origin? No Set COOP same-origin Yes COEP header present? No Add COEP require-corp Yes A subresource is blocked? Yes Add CORP or use credentialless No Inspect embedding frame a parent without isolation drags the child down
Diagnose a false flag in evaluation order: COOP value, then COEP presence, then blocked subresources, then the embedding chain.

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

require-corp versus credentialless A matrix comparing how the two COEP values handle a third-party asset without CORP, cross-origin cookies, the isolation grant, and their best use case. Behaviour require-corp credentialless Third-party asset, no CORP Blocked Loads Cross-origin cookies Sent Stripped crossOriginIsolated Granted Granted Best when You control assets Uncontrolled CDN
Both values unlock isolation; they differ only in how uncontrolled cross-origin assets and their cookies are treated.

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.