Blog · 2026-06-20 · COOP · COEP · Cross-Origin Isolation · MCP Servers
MCP Server COOP/COEP Cross-Origin Isolation: SharedArrayBuffer Availability, Spectre Mitigations, CORP Per-Resource Headers, and OAuth Popup Flows
MCP servers that render tool output in iframes, use SharedArrayBuffer-backed worker pools for performance, and authenticate users through OAuth popups face a three-way tension: Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy together unlock SharedArrayBuffer and Atomics.wait(), but COOP: same-origin severs the window.opener reference that every OAuth popup flow depends on, and COEP: require-corp blocks Google Analytics, Stripe.js, and every other third-party script that doesn't send its own Cross-Origin-Resource-Policy header. Getting all three workloads running simultaneously requires understanding exactly which header combination achieves window.crossOriginIsolated === true, which doesn't, and what each trade-off costs.
Why COOP and COEP matter specifically for MCP servers
MCP servers are not uniform HTTP APIs. The most sophisticated deployments include a browser-facing UI component that renders structured tool output — markdown, code, tables, and sometimes sandboxed iframes for untrusted content. They run background worker threads to process large tool responses without blocking the main thread. And they integrate OAuth flows to delegate credential acquisition to identity providers. Each of these three workloads pulls the header configuration in a different direction.
Tool output rendering in iframes. When an MCP server embeds tool output in a child <iframe>, the parent page's COEP header determines whether that embedding is permitted under cross-origin isolation. An iframe that does not send its own COEP header will be blocked by a parent with COEP: require-corp unless the iframe's origin explicitly opts in via Cross-Origin-Resource-Policy: cross-origin. This creates a deployment dependency on resources you may not control.
SharedArrayBuffer in worker pools. After the Spectre vulnerability class was disclosed in 2018, all major browsers withdrew SharedArrayBuffer from the default web platform. The API was later restored, but only in pages where window.crossOriginIsolated evaluates to true. MCP servers that use SAB-backed shared memory for zero-copy data transfer between worker threads — a legitimate performance optimization for large tool responses — cannot use SAB without the correct header combination. Calling new SharedArrayBuffer(n) in a non-isolated context throws a TypeError with no graceful fallback.
OAuth flows in popups. OAuth 2.0 authorization code and implicit flows commonly use a popup window pattern: the application opens the identity provider in a popup, the IdP redirects to a callback URL, and the callback page writes the authorization code or token back to the opener via window.opener.postMessage() or by navigating window.opener.location. COOP: same-origin destroys the window.opener reference between cross-origin windows — the popup can no longer communicate back to the application frame. This breaks the popup pattern entirely unless you know the workaround.
The fundamental constraint: window.crossOriginIsolated requires COOP: same-origin AND either COEP: require-corp or COEP: credentialless. COOP: same-origin-allow-popups does not achieve cross-origin isolation regardless of the COEP value. There is no configuration that simultaneously satisfies all three workloads with a single header pair — you must choose which to prioritize and apply compensating controls for the others.
COOP mechanics: controlling the browsing context group
Cross-Origin-Opener-Policy is a response header that controls whether a document shares its browsing context group with cross-origin openers and openees. The browsing context group is the shared address-space boundary within the browser: documents in the same group can access each other's window objects. Documents in different groups are process-isolated from each other.
COOP: same-origin
The strictest value. When a page is served with Cross-Origin-Opener-Policy: same-origin, the browser places it in a new browsing context group the moment it is navigated to — unless the opener is from the same origin and also sends COOP: same-origin. All cross-origin windows lose the window.opener reference. Navigating via window.open() to any cross-origin URL, or being opened by a cross-origin window, results in window.opener === null on both ends.
This is the value required for crossOriginIsolated. It is also the value that breaks OAuth popup flows. When your application at app.skillaudit.dev calls window.open('https://accounts.google.com/o/oauth2/v2/auth?...'), the Google auth page is cross-origin. Under COOP: same-origin, the popup cannot write back to window.opener after the OAuth redirect — the reference is null.
// Express middleware: COOP same-origin — enables crossOriginIsolated, breaks OAuth popups
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
next();
});
// In the browser — verify isolation before using SAB
if (window.crossOriginIsolated) {
const sab = new SharedArrayBuffer(1024 * 1024); // OK
} else {
console.error('crossOriginIsolated is false — SAB unavailable');
}
COOP: same-origin-allow-popups
A middle-ground value. The page is isolated from pages that open it, but it retains the window.opener reference to popups it opens. This allows an application to open an OAuth popup and retain the ability to receive postMessage calls from that popup via the opener reference chain.
The critical limitation: same-origin-allow-popups does not achieve crossOriginIsolated === true. The browser spec requires COOP: same-origin (not the allow-popups variant) in combination with a COEP header. Any attempt to allocate a SharedArrayBuffer in a page with COOP: same-origin-allow-popups will throw a TypeError.
// COOP same-origin-allow-popups — OAuth popups work, crossOriginIsolated remains false
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin-allow-popups');
next();
});
// In the browser
console.log(window.crossOriginIsolated); // false — SAB not available
// OAuth popup flow works:
const popup = window.open('https://accounts.google.com/o/oauth2/v2/auth?...', '_blank');
// popup.opener is NOT null on the callback page, so postMessage to opener succeeds
COOP: unsafe-none (default)
The default value when no COOP header is sent. All windows share browsing context groups freely. OAuth popups, third-party scripts, and cross-origin iframes all operate without restriction. crossOriginIsolated is false. SharedArrayBuffer is unavailable. This is the pre-2018 web — Spectre is unmitigated at the browser boundary. MCP servers that require SAB for worker performance must not rely on this default.
// No COOP header set — unsafe-none is the default // crossOriginIsolated === false, SharedArrayBuffer unavailable // Spectre timing attack not mitigated by process isolation
COEP mechanics: controlling cross-origin resource loading
Cross-Origin-Embedder-Policy is a document-level policy that restricts which cross-origin resources the document is permitted to load. It acts as a declaration that the document only embeds resources that have explicitly opted in to cross-origin embedding, which allows the browser to safely grant the document access to high-resolution timers and shared memory.
COEP: require-corp
The original and strictest value. Under require-corp, every cross-origin resource the page loads — scripts, images, fonts, stylesheets, iframes, workers — must respond with a Cross-Origin-Resource-Policy header that explicitly permits the embedding. Resources without a CORP header are blocked at the network layer and treated as load failures.
This immediately breaks every third-party dependency that doesn't send CORP: cross-origin: Google Analytics, Stripe.js, Intercom, Segment, most CDN-hosted fonts, and most advertisement networks. As of mid-2026, the majority of commonly embedded third-party scripts do not send CORP headers.
// Express middleware: COEP require-corp — blocks third-party resources without CORP header
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});
// Combine with COOP for full cross-origin isolation:
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});
// Now window.crossOriginIsolated === true
// SharedArrayBuffer available, Atomics.wait() available
// BUT: Google Analytics, Stripe.js, Intercom will fail to load
COEP: credentialless
A newer and more practical alternative, now supported in all major browsers. Under credentialless, cross-origin no-cors requests are sent without cookies or stored credentials — they are treated as anonymous requests. Resources served from CDNs without a CORP header will load, as long as they don't require cookies to serve the correct content.
The key distinction: credentialless does not require every cross-origin resource to send CORP. Subresource requests are stripped of credentials and sent anonymously. If the CDN responds, the response is used. If the response requires authentication (e.g., a behind-auth analytics beacon), it silently fails.
Combined with COOP: same-origin, COEP: credentialless does achieve crossOriginIsolated === true. This is the recommended path for MCP servers that use SAB but also depend on third-party analytics or CDN assets.
// COEP credentialless — cross-origin resources load without credentials, no CORP required
// Combined with COOP same-origin: achieves crossOriginIsolated === true
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');
next();
});
// Startup guard — always check before allocating SAB
if (!crossOriginIsolated) {
throw new Error('SAB requires cross-origin isolation: COOP same-origin + COEP require-corp or credentialless');
}
const sharedBuffer = new SharedArrayBuffer(4 * 1024 * 1024); // 4 MB worker pool buffer
Choosing between require-corp and credentialless: Use require-corp when you have full control over all resources the page loads and can add CORP: cross-origin to every external dependency. Use credentialless when you depend on third-party CDN assets or analytics scripts that you cannot control — but understand that those resources will be fetched without cookies, which may affect personalization or authenticated CDN paths.
CORP: Cross-Origin-Resource-Policy as per-resource opt-in
Cross-Origin-Resource-Policy is a response header set on individual resources — not on the document that loads them. It controls which origins are permitted to load that resource cross-origin when COEP is in effect. The three values map to different access levels and serve distinct purposes in an MCP server deployment.
CORP: same-origin
The resource may only be loaded by documents from the same origin. This is the correct value for MCP API endpoints, JSON data responses, and any resource that should never be loaded by a third-party page. Setting CORP: same-origin on your MCP tool invocation endpoint prevents any cross-origin page from making a no-cors request that reads the response content.
// Express: CORP same-origin on MCP API endpoints
app.use('/api/mcp', (req, res, next) => {
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
next();
});
CORP: same-site
The resource may be loaded by any document from the same eTLD+1 (registered domain), regardless of subdomain. This is the correct value for resources served from CDN subdomains under the same domain: assets.skillaudit.dev serving resources to app.skillaudit.dev should send CORP: same-site. Both share the skillaudit.dev registered domain, so same-site allows the embedding while preventing cross-domain access.
// Express on CDN subdomain: CORP same-site for intra-domain assets
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Resource-Policy', 'same-site');
next();
});
CORP: cross-origin
The resource may be loaded by any origin. This is the correct value for genuinely public resources: web fonts, public images, openly downloadable assets. It is the wrong value for MCP API endpoints — setting CORP: cross-origin on a tool invocation endpoint means any cross-origin page can make a no-cors request and the browser will not suppress the response.
| CORP Header Value | Who Can Embed | MCP Server Use Case | Risk if Misapplied |
|---|---|---|---|
same-origin |
Only exact same origin (scheme://host:port) |
MCP tool API endpoints, session-authenticated JSON responses, private tool schemas | None — most restrictive; correct default for API resources |
same-site |
Any subdomain of the same registered domain (eTLD+1) | CDN subdomains serving app assets, static JS/CSS on assets.example.com |
Resources accessible to any subdomain — verify all subdomains are trusted |
cross-origin |
Any origin on the internet | Public web fonts, open-access images, publicly downloadable static files | MCP API endpoints with cross-origin allow cross-site response reading in no-cors requests |
| (absent) | Under COEP: require-corp — blocked; Under COEP: credentialless — loaded anonymously | Third-party CDN scripts before you can control them; legacy resources during migration | Blocked in cross-origin isolated contexts under require-corp; silently degrades under credentialless |
crossOriginIsolated and SharedArrayBuffer: the post-Spectre model
Spectre is a CPU microarchitectural vulnerability that allows code within a CPU core to read arbitrary memory from the same process via timing side-channels. In a browser, JavaScript from multiple origins may share a renderer process — meaning a Spectre exploit in one page's JavaScript could read secrets from another origin's memory within the same process.
Browsers responded in 2018 by removing two platform primitives that Spectre exploits depend on: SharedArrayBuffer (which allows precise shared-memory timing) and high-resolution timers (performance.now() sub-millisecond accuracy). Both were withdrawn from the platform. SharedArrayBuffer was later reinstated, but only for pages where the browser has already placed the document in a dedicated renderer process — meaning no other cross-origin content shares that process's address space.
The window.crossOriginIsolated property is the browser's API for checking whether the current page is in that isolated renderer process. It evaluates to true only when:
- The document was served with
Cross-Origin-Opener-Policy: same-origin(not allow-popups, not unsafe-none) - The document was served with
Cross-Origin-Embedder-Policy: require-corpORCross-Origin-Embedder-Policy: credentialless - The browser has implemented and enforced both headers (all modern browsers as of 2026)
When crossOriginIsolated is true, the browser also re-enables high-resolution timers and Atomics.wait() in worker threads. MCP server worker pools that use SAB for inter-thread communication need all three: the buffer itself, the ability to block on it, and the guaranteed process isolation that makes timing attacks impractical.
// Worker pool initialization — guard SAB allocation at startup
// workers/pool.js
import { Worker } from 'worker_threads';
function createWorkerPool(size) {
// Runtime check before any SAB allocation
if (typeof crossOriginIsolated !== 'undefined' && !crossOriginIsolated) {
throw new Error(
'SharedArrayBuffer requires cross-origin isolation.\n' +
'Set COOP: same-origin and COEP: require-corp or credentialless.\n' +
'Current crossOriginIsolated: ' + crossOriginIsolated
);
}
// 4 MB shared control block: [workerCount, queueHead, queueTail, ...slots]
const controlBlock = new SharedArrayBuffer(4 * 1024 * 1024);
const controlView = new Int32Array(controlBlock);
const workers = Array.from({ length: size }, (_, i) => {
const w = new Worker('./workers/mcp-tool-worker.js', {
workerData: { workerId: i, controlBlock }
});
w.on('error', (err) => {
console.error(`Worker ${i} error:`, err);
});
return w;
});
return { workers, controlBlock, controlView };
}
export { createWorkerPool };
// workers/mcp-tool-worker.js — uses Atomics.wait() on the shared buffer
import { workerData, parentPort } from 'worker_threads';
const { workerId, controlBlock } = workerData;
const controlView = new Int32Array(controlBlock);
// Atomics.wait() blocks this worker thread until the main thread signals
// This is only available when crossOriginIsolated === true on the document
function waitForTask(index, expectedValue, timeoutMs = 5000) {
const result = Atomics.wait(controlView, index, expectedValue, timeoutMs);
if (result === 'timed-out') {
throw new Error(`Worker ${workerId}: Atomics.wait timed out after ${timeoutMs}ms`);
}
return result;
}
parentPort.on('message', (msg) => {
if (msg.type === 'PROCESS_TOOL_OUTPUT') {
// Process tool output using shared buffer for zero-copy transfer
const outputView = new Uint8Array(controlBlock, 16, msg.length);
// ... processing logic
Atomics.store(controlView, 2, 1); // signal done
Atomics.notify(controlView, 2, 1);
parentPort.postMessage({ type: 'DONE', workerId });
}
});
Never assume crossOriginIsolated is true. Browser behavior, CDN misconfiguration, or a response header stripped by a reverse proxy can silently change crossOriginIsolated to false in production. Always check at startup, throw an explicit error, and surface it in your monitoring pipeline — do not allow the application to silently fall back to a non-SAB code path that hasn't been tested.
Third-party script interaction under COEP
The most immediate operational impact of enabling COEP on an MCP server UI is the breakage of third-party scripts. The four most common categories are analytics (Google Analytics, Segment, Mixpanel), payment widgets (Stripe.js), customer support chat (Intercom, Zendesk), and social embed buttons. As of mid-2026, none of the leading implementations of any of these categories send CORP: cross-origin response headers on their script files.
Under COEP: require-corp, these scripts produce a network error in the browser console:
// Browser console under COEP: require-corp // A script tag loading https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX // produces: // Cross-Origin-Embedder-Policy: require-corp blocked loading of 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX' // because it lacks a 'Cross-Origin-Resource-Policy' header.
There are three workable solutions, each with a different set of trade-offs:
Solution A: Switch to COEP: credentialless
The lowest-effort path. Replace require-corp with credentialless. Third-party scripts will load — they will be fetched without cookies, but most analytics and support scripts work correctly without cookies on the script request itself (they set their own cookies after loading). You still achieve crossOriginIsolated === true with COOP: same-origin.
// Switch from require-corp to credentialless to unblock third-party scripts
// while preserving crossOriginIsolated
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless'); // was require-corp
next();
});
Solution B: Proxy third-party scripts via a same-origin service worker
For cases where the third-party script requires cookie authentication to serve the correct variant, you can intercept the request in a service worker and add the CORP: cross-origin header to the proxied response before it reaches the document's COEP check.
// service-worker.js — intercept third-party analytics requests
// and add CORP header to enable loading under COEP: require-corp
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Proxy Google Tag Manager through the service worker
if (url.hostname === 'www.googletagmanager.com') {
event.respondWith(
fetch(event.request).then((response) => {
const headers = new Headers(response.headers);
headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
})
);
}
});
Solution C: Load via sandboxed iframe without COEP inheritance
The <iframe sandbox> attribute creates a sandboxed browsing context that does not inherit the parent's COEP. Third-party scripts loaded inside a sandboxed iframe are isolated from the parent document's cross-origin isolation status — they operate under their own default policies.
<!-- Sandboxed iframe for third-party widgets —
does NOT inherit parent COEP or COOP.
Not cross-origin-isolated. Use only for display widgets. -->
<iframe
src="/widget-host.html"
sandbox="allow-scripts allow-same-origin"
width="300"
height="250"
style="border:none"
></iframe>
Important: A sandboxed iframe with allow-same-origin does not inherit the parent's COEP. The iframe is not cross-origin-isolated even if the parent is. Do not attempt to use SharedArrayBuffer inside a sandboxed iframe — it will throw even if the parent page has crossOriginIsolated === true. The isolation guarantee applies only to the document that set the COOP/COEP headers, not to its children.
OAuth popup flows under COOP: the broken pattern and two fixes
The standard OAuth 2.0 authorization code flow with a popup works like this: the application opens the identity provider URL in a new window via window.open(). The user authenticates at the IdP, which redirects to the application's OAuth callback URL. The callback page reads the authorization code from the URL, then passes it back to the opener via window.opener.postMessage() or by invoking window.opener.location.replace().
This pattern relies on the popup being able to access window.opener after the redirect. Under COOP: same-origin, it cannot.
Application opens OAuth popup. window.open('https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=https://app.skillaudit.dev/callback&...') — popup opens with window.opener pointing back to the application frame.
User authenticates at Google. Google's authorization server is cross-origin from app.skillaudit.dev. The browsing context group is already different — window.opener is null from the Google frame's perspective under COOP.
Google redirects to OAuth callback. The popup navigates from accounts.google.com to app.skillaudit.dev/callback?code=xxx. Under COOP: same-origin, the navigation between cross-origin documents in the same popup closes the opener reference.
Callback attempts to contact opener. window.opener.postMessage({ code: 'xxx' }, 'https://app.skillaudit.dev') — throws TypeError: Cannot read properties of null (reading 'postMessage'). The authorization code cannot be delivered to the application.
Fix A: COOP asymmetry — same-origin on callback, same-origin-allow-popups on app
The application page (the one that opens the popup) sends COOP: same-origin-allow-popups. The OAuth callback page (the one that returns from the IdP) sends COOP: same-origin. This combination preserves the opener reference while the popup is in the same browsing context group as the application.
The critical detail: the COOP value on the callback page must match across navigation for the browsing context group to stay merged. When the popup navigates from the cross-origin IdP back to your same-origin callback, the browser creates a new browsing context group for the callback — but because the opener (same-origin-allow-popups) allows same-origin popups, the reference is preserved at the callback step.
// app page (the one that opens the OAuth popup):
// Cross-Origin-Opener-Policy: same-origin-allow-popups
// NOTE: crossOriginIsolated will be false — SAB not available on this page
app.get('/dashboard', (req, res) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin-allow-popups');
res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');
// crossOriginIsolated === false because allow-popups prevents it
res.sendFile('dashboard.html');
});
// OAuth callback page (the popup destination after IdP redirect):
// Cross-Origin-Opener-Policy: same-origin
app.get('/oauth/callback', (req, res) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
const { code, state } = req.query;
// Validate state parameter against session cookie
if (!validateState(state, req.session.oauthState)) {
return res.status(400).send('Invalid state parameter');
}
// Return page that posts message to opener
res.send(`
<!doctype html>
<html><body><script>
if (window.opener) {
window.opener.postMessage(
{ type: 'OAUTH_CODE', code: ${JSON.stringify(code)} },
'https://app.skillaudit.dev'
);
window.close();
} else {
// Fallback: store in sessionStorage, redirect back
sessionStorage.setItem('oauth_code', ${JSON.stringify(code)});
window.location.href = '/dashboard?oauth=complete';
}
</script></body></html>
`);
});
Fix B: Redirect-based OAuth (no popup required)
The cleanest solution that requires no COOP relaxation. Instead of opening a popup, the application redirects the entire page to the IdP, stores the pre-redirect state (PKCE verifier, original URL, CSRF nonce) in an encrypted session cookie, completes the OAuth flow as a full-page navigation, then redirects back to the original URL after the callback. The popup pattern only exists for UX reasons — redirect-based OAuth is the OAuth 2.0 default and is unaffected by COOP entirely.
// Redirect-based OAuth — COOP: same-origin compatible, no popup needed
import { randomBytes, createHash } from 'crypto';
import { Router } from 'express';
const oauthRouter = Router();
oauthRouter.get('/login', (req, res) => {
// Generate PKCE code verifier and challenge
const codeVerifier = randomBytes(32).toString('base64url');
const codeChallenge = createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// Store in session — survives the redirect
req.session.pkceVerifier = codeVerifier;
req.session.oauthState = randomBytes(16).toString('hex');
req.session.returnTo = req.query.returnTo || '/dashboard';
const params = new URLSearchParams({
client_id: process.env.OAUTH_CLIENT_ID,
redirect_uri: 'https://app.skillaudit.dev/oauth/callback',
response_type: 'code',
scope: 'openid profile email',
state: req.session.oauthState,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
// Full-page redirect — no popup, COOP: same-origin is fine
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
oauthRouter.get('/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.oauthState) {
return res.status(400).send('State mismatch — CSRF check failed');
}
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://app.skillaudit.dev/oauth/callback',
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
code_verifier: req.session.pkceVerifier
})
});
const tokens = await tokenResponse.json();
req.session.accessToken = tokens.access_token;
req.session.idToken = tokens.id_token;
const returnTo = req.session.returnTo || '/dashboard';
delete req.session.pkceVerifier;
delete req.session.oauthState;
delete req.session.returnTo;
res.redirect(returnTo);
});
export { oauthRouter };
Redirect-based OAuth + COOP: same-origin + COEP: credentialless = full cross-origin isolation with working OAuth. This is the configuration SkillAudit recommends as the baseline for MCP server deployments that need SAB. The popup pattern requires COOP relaxation; the redirect pattern does not. Migrate to redirect-based flows before enabling strict COOP.
Process isolation as the Spectre mitigation
Understanding why COOP and COEP actually mitigate Spectre — rather than merely obscuring it — requires understanding what process isolation achieves at the OS level.
Spectre exploits rely on the attacker's code running in the same CPU core context as the victim's data, and being able to measure timing differences caused by speculative execution filling CPU caches with victim memory. The mitigation at the OS and browser level is to ensure that attacker and victim code never share a renderer process: if they run in separate processes with separate address spaces, speculative reads across the process boundary are prevented by the CPU's memory protection hardware.
Chromium's Site Isolation feature — enabled by default since Chrome 67 — places each site (eTLD+1) in its own renderer process. This prevents cross-site Spectre attacks. But cross-origin isolation, enforced via COOP and COEP, goes further: it ensures that even same-site, cross-origin documents are placed in dedicated renderer processes. An MCP server at app.skillaudit.dev and a potentially hostile page at cdn.skillaudit.dev are same-site but cross-origin. Site Isolation may or may not separate them; cross-origin isolation via COOP/COEP guarantees they are separated.
The relationship between Site Isolation and cross-origin isolation:
| Mechanism | Enabled By | Process Boundary | SAB Available | Covers Same-Site Cross-Origin? |
|---|---|---|---|---|
| Site Isolation | Browser default (Chrome 67+, Firefox, Safari) | Per eTLD+1 | No (SAB still restricted) | No — same eTLD+1 shares process |
| Cross-Origin Isolation (COOP + COEP) | COOP: same-origin + COEP: require-corp or credentialless | Per origin (scheme+host+port) | Yes — crossOriginIsolated === true |
Yes — each origin in its own process |
The practical implication for MCP servers: you cannot rely on Site Isolation alone to justify SAB usage. Site Isolation is a browser default that the server has no control over, and it doesn't achieve the crossOriginIsolated property. Only the explicit COOP+COEP header pair does. See also MCP server Permissions Policy security for the related problem of feature policy inheritance across origins.
SkillAudit findings for COOP/COEP misconfigurations
SkillAudit's header analysis engine checks both server responses and client-side JavaScript for COOP/COEP configuration gaps. The following findings appear most frequently in MCP server audits:
No
Cross-Origin-Opener-Policy header present on any document response. The page will never achieve crossOriginIsolated === true regardless of COEP configuration. SharedArrayBuffer is permanently unavailable and Spectre timing mitigations are not in effect. Remediation: set COOP: same-origin on all document responses; migrate OAuth to redirect-based flow if popup OAuth was in use.JavaScript source allocates
new SharedArrayBuffer(n) or calls Atomics.wait() without a preceding if (!crossOriginIsolated) throw guard. In production environments without COOP/COEP, the allocation will throw an unhandled TypeError and crash the worker pool initialization. Remediation: add an explicit startup guard; log crossOriginIsolated status to your observability pipeline.COEP: require-corp is set but one or more critical third-party dependencies (analytics, payment, support widgets) do not send a CORP: cross-origin header. These resources will fail to load in production, causing silent analytics gaps or broken payment flows. Remediation: switch to COEP: credentialless, or proxy dependencies via service worker with synthetic CORP header injection.COOP: same-origin is set on the application page, and OAuth is implemented as a popup flow. The OAuth callback's window.opener.postMessage() call will throw a TypeError at runtime. No fallback path detected. Remediation: migrate to redirect-based OAuth (preferred), or apply COOP asymmetry: same-origin-allow-popups on the application page, same-origin on the callback page.COEP: credentialless is correctly set but application JavaScript does not check crossOriginIsolated before allocating SharedArrayBuffer. A misconfigured reverse proxy stripping the COEP header would cause a runtime crash. Remediation: add if (!crossOriginIsolated) throw new Error('...') before first SAB allocation.An MCP tool invocation endpoint or authenticated data endpoint responds with
Cross-Origin-Resource-Policy: cross-origin. This permits any origin on the internet to make a no-cors request and have the response loaded by their document. Combined with COEP on the attacker's page, response content may be readable. Remediation: restrict API endpoints to CORP: same-origin; use cross-origin only for genuinely public static assets.Deployment checklist
- Set
Cross-Origin-Opener-Policy: same-originon all document responses wherecrossOriginIsolatedis required; usesame-origin-allow-popupsonly on pages that must open OAuth popups - Set
Cross-Origin-Embedder-Policy: credentialless(preferred overrequire-corp) to achieve cross-origin isolation without breaking third-party CDN dependencies - Verify
window.crossOriginIsolated === trueat application startup; throw an explicit error and emit an alert if it is not — do not silently continue - Guard every
new SharedArrayBuffer(n)call with anif (!crossOriginIsolated) throwcheck; include the check in CI tests with a mocked header environment - Set
Cross-Origin-Resource-Policy: same-originon all MCP tool API endpoints and authenticated JSON responses - Set
Cross-Origin-Resource-Policy: same-siteon assets served from CDN subdomains under the same registered domain - Reserve
Cross-Origin-Resource-Policy: cross-originfor genuinely public static assets only — never for endpoints that handle authenticated requests - Migrate OAuth flows from popup pattern to redirect-based PKCE flow to eliminate the COOP tension entirely; implement state parameter validation and PKCE on every OAuth callback
- If popup OAuth is unavoidable: serve the OAuth callback page with
COOP: same-originand the application page withCOOP: same-origin-allow-popups; add awindow.opener === nullfallback path in the callback script - Test COOP/COEP headers are not stripped by your reverse proxy, CDN, or load balancer — add header presence assertions to your smoke test suite on every deployment
- Use
Cross-Origin-Opener-Policy-Report-OnlyandCross-Origin-Embedder-Policy-Report-Onlywith areport-toendpoint for staged rollout — collect violations in staging before enabling enforcement in production - Run SkillAudit header analysis on every deploy to catch COOP/COEP regressions introduced by header configuration changes in middleware chains or CDN rules
SkillAudit check: SkillAudit's automated scanner validates COOP/COEP header presence and value correctness, checks JavaScript source for SAB usage without crossOriginIsolated guards, identifies CORP header misconfiguration on API endpoints, and detects OAuth popup patterns under COOP: same-origin. Audit your MCP server →
See also: MCP server Trusted Types API security (DOM XSS sinks, createHTML policy enforcement) · MCP server worker thread message security (postMessage structured clone, SharedArrayBuffer races) · MCP server CORS credential security (Access-Control-Allow-Credentials, preflight bypass) · MCP server Permissions Policy security (feature policy inheritance, iframe delegation)
Scan your MCP server for COOP/COEP misconfiguration
SkillAudit detects missing cross-origin isolation headers, SharedArrayBuffer usage without guards, CORP mismatches on API endpoints, and OAuth popup patterns broken by COOP. Get a free audit in 60 seconds.
Free audit →