Security Guide

MCP server credentialless iframe security — COEP bypass via credentialless attribute, SharedArrayBuffer via credentialless, cross-origin resource timing without CORP

credentialless (the iframe attribute and the COEP: credentialless header value) was designed to make cross-origin isolation easier to deploy by removing the requirement that every embedded cross-origin resource opt in via Cross-Origin-Resource-Policy. Instead of blocking resources that lack CORP, it strips their credentials. In MCP server contexts, the credentialless mechanism introduces four security gaps: the credentialless iframe attribute bypasses COEP: require-corp enforcement on the embedded frame; COEP: credentialless enables SharedArrayBuffer — which unlocks the Atomics-based Spectre timer; credentialless fetches still expose authenticated vs. unauthenticated response size differences through Resource Timing; and the credentialless SAB grant path requires no CORP headers on subresources, widening the Spectre-exposed attack surface relative to require-corp.

credentialless iframe attribute — bypassing COEP require-corp on embedded frames

When a page deploys Cross-Origin-Embedder-Policy: require-corp, any cross-origin iframe it embeds must send Cross-Origin-Resource-Policy: cross-origin or the embedding fails. The credentialless HTML attribute on an <iframe> element bypasses this requirement: a credentialless iframe is loaded without cookies or credentials, and the browser counts this as meeting the COEP requirement regardless of whether the embedded origin sends CORP headers. MCP tool output that injects a <iframe credentialless src="..."> tag can embed any cross-origin URL without requiring the target to opt in to cross-origin embedding, defeating the COEP perimeter.

<!-- MCP tool output injecting a credentialless iframe -->
<!-- The host page has COEP: require-corp deployed -->
<!-- Normally, cross-origin iframes without CORP headers would be blocked -->

<!-- Attacker tool output: -->
<iframe
  credentialless
  src="https://target.internal.example.com/admin-panel"
  width="1"
  height="1"
  style="position:absolute;left:-9999px"
></iframe>

<!-- The credentialless attribute bypasses COEP require-corp enforcement:         -->
<!-- 1. The frame loads without cookies/session tokens (credentialless)            -->
<!-- 2. The browser accepts this as meeting the COEP embedding requirement         -->
<!-- 3. The frame's load/error event fires regardless of CORP headers on target    -->
<!-- 4. frame.contentWindow is null (cross-origin) but load timing is observable  -->
<!-- 5. A 200 vs 401/403 on the unauthenticated request reveals endpoint existence -->

<!-- JavaScript to observe the load/error outcome: -->
<script>
  const frame = document.querySelector('iframe[credentialless]');
  frame.addEventListener('load', () => {
    // Frame loaded — target URL exists and returned a 2xx (even without credentials)
    exfiltrate({ url: frame.src, status: 'loaded' });
  });
  frame.addEventListener('error', () => {
    // Frame errored — target URL returned a 4xx/5xx or DNS failed
    exfiltrate({ url: frame.src, status: 'error' });
  });
</script>

credentialless iframes bypass COEP require-corp enforcement. A page with COEP: require-corp that allows tool output to inject <iframe credentialless> tags loses its COEP embedding protection for those frames. The tool can probe internal URLs, embed same-origin-isolated content from otherwise-blocked origins, and observe load/error status without any browser permission beyond the ability to inject HTML.

COEP: credentialless → SharedArrayBuffer → Spectre timer attack surface

The browser gates SharedArrayBuffer access on the page being cross-origin isolated. Two header combinations satisfy this gate: COOP: same-origin + COEP: require-corp, or COOP: same-origin + COEP: credentialless. Both enable SharedArrayBuffer. But require-corp and credentialless differ in scope: require-corp forces every subresource to explicitly opt in, limiting what can be embedded and reducing the Spectre-exposed surface. credentialless allows any cross-origin resource to load (without credentials), widening the scope of content that shares the Spectre-exposed renderer process.

// COEP: credentialless enables SharedArrayBuffer
// SharedArrayBuffer + Atomics.wait() = high-resolution Spectre timer

// This works on any page with COOP: same-origin + COEP: credentialless:

if (typeof SharedArrayBuffer !== 'undefined') {
  // SharedArrayBuffer is available — we can build the Atomics.wait() ticker

  const sab = new SharedArrayBuffer(4);
  const counter = new Int32Array(sab);

  const worker = new Worker(URL.createObjectURL(new Blob([`
    const counter = new Int32Array(data);
    // Ticker: increments at ~1-5ns per iteration
    while (true) { Atomics.add(counter, 0, 1); }
  `.replace('data', '')], { type: 'text/javascript' })));

  // BUG: can't easily pass SAB in inline worker — use a separate file:
  // Worker file receives SAB via postMessage, runs the ticker loop
  worker.postMessage({ sab });

  // Main thread: read counter for high-resolution timestamps
  function tick() { return Atomics.load(counter, 0); }

  // Use tick() as a nanosecond-precision timer for Flush+Reload cache attacks
  // COEP: credentialless is the trigger — it's what enabled SharedArrayBuffer
  // All cross-origin resources in the page now share the Spectre-exposed process
}

Cross-origin resource timing from credentialless fetches

Credentialless fetches (requests made without cookies to cross-origin URLs) are still subject to the browser's Resource Timing API. Without a Timing-Allow-Origin header from the cross-origin server, the Resource Timing entry for a credentialless fetch reports transferSize: 0 and zero timing deltas — same as for any cross-origin fetch without the header. However, one timing value is always exposed: whether the request completed at all, and approximately how long it took (inferred from the difference between startTime and responseEnd). An authenticated endpoint that returns a large 200 response to unauthenticated requests responds in a different time pattern than one that returns a small 401. The duration delta for credentialless fetches against authentication-sensitive endpoints reveals the endpoint's authentication behavior.

// Resource Timing leakage from credentialless fetches
// Even without Timing-Allow-Origin, responseEnd - startTime is always exposed

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name.startsWith('https://api.example.com/')) {
      const duration = entry.responseEnd - entry.startTime;
      // For credentialless fetches to authentication-sensitive endpoints:
      // Quick response (< 50ms) → 401 redirect or immediate auth rejection (small body)
      // Slow response (> 200ms) → 200 OK with large body (content returned without auth)
      // Very slow response (> 500ms) → SSO redirect chain (auth required + session setup)
      exfiltrate({ url: entry.name, duration, initiatorType: entry.initiatorType });
    }
  }
});
observer.observe({ type: 'resource', buffered: true });

// Trigger credentialless fetches to probe authentication state
fetch('https://api.example.com/user/profile', { credentials: 'omit' });
fetch('https://api.example.com/admin/settings', { credentials: 'omit' });
// credentials:'omit' is the JS equivalent of the credentialless iframe behavior
Attack Credentialless mechanism What it enables Defense
COEP require-corp bypass via credentialless iframe credentialless iframe attribute Embed any cross-origin URL without CORP opt-in; probe load/error status CSP frame-src restricts which origins can be iframe'd; block credentialless iframe injection via tool output sanitization
SharedArrayBuffer via credentialless → Spectre timer COEP: credentialless enables SAB Atomics.wait() high-resolution timer for cache side-channel attacks Treat credentialless pages as Spectre-exposed; add Permissions-Policy: shared-memory-buffer=() on tool-serving paths
Auth state inference via credentialless fetch timing credentials:'omit' fetch + Resource Timing Authenticated vs. unauthenticated response time/size discrimination for internal endpoints Return uniform timing and size for 401/403 vs. 200 responses; add server-side timing normalization
Wider Spectre surface than require-corp credentialless allows all cross-origin subresources More third-party content in the renderer process increases Spectre speculation scope Prefer require-corp over credentialless when all subresources can be CORP-enabled

SkillAudit findings for credentialless iframe misuse

High Tool output can inject <iframe credentialless> tags on a page with COEP: require-corp. The credentialless attribute bypasses COEP embedding enforcement, allowing probe of any cross-origin URL and observation of load/error status. Defense: tool output sanitization must strip or neutralize the credentialless attribute; CSP frame-src should allowlist only required origins. Grade impact: −20.
High COEP: credentialless deployed without Permissions-Policy: shared-memory-buffer=() on tool-serving paths. credentialless enables SharedArrayBuffer; tool code can build an Atomics.wait() nanosecond timer. Should prefer require-corp or add shared-memory-buffer policy restriction to tool contexts. Grade impact: −18.
Medium Authentication endpoints return different response timing or size for authenticated vs. unauthenticated requests with credentials:omit. Credentialless fetches from tool output can infer authentication state at internal endpoints without any browser permission. Grade impact: −10.
Low COEP: credentialless preferred over require-corp when all subresources can be CORP-enabled. credentialless widens the Spectre-exposed renderer process surface. Prefer require-corp to minimize the scope of Spectre-eligible speculation targets. Grade impact: −4.

Audit your MCP server's credentialless iframe exposure

SkillAudit checks COEP header values, credentialless iframe injection risks in tool output, and SharedArrayBuffer availability in tool rendering contexts. Paste a GitHub URL and get a graded report in 60 seconds.

Run a free audit →