13. COEP credentialless over require-corp

Status

Accepted — 2026-04-19

Context

The blog and sludge-report hosting targets serve strict cross-origin isolation headers: Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: same-origin, and Cross-Origin-Embedder-Policy: require-corp. These were added in commit 3f6945f based on a Bastion security header audit, which recommended the strongest available posture.

require-corp tells the browser to block any cross-origin resource whose response does not include a Cross-Origin-Resource-Policy: cross-origin header. This works when you control every server your site talks to. The blog does not: it loads Google Tag Manager, Firebase Performance Monitoring telemetry, Firestore realtime listener channels, Google Fonts, and content pipeline WASM modules (Shiki/Mermaid). None of these Google-operated endpoints send CORP headers.

The result was five categories of runtime error in production:

  1. Google Tag Manager script blocked — COEP rejected the gtag.js fetch.
  2. CSP base-uri 'none' violation — Angular’s <base href="/"> was blocked (a CSP issue surfaced during the same investigation).
  3. WebAssembly instantiation failure — content pipeline WASM blocked by COEP and missing wasm-unsafe-eval in CSP script-src.
  4. Firebase logging 504 spiralfirebaselogging-pa.googleapis.com POST blocked by COEP, then retried with exponential backoff, producing tens of thousands of 504 stack traces per session.
  5. Firestore listener terminate blocked — realtime listener close requests rejected by CORP via the service worker fetch handler.

Errors 4 and 5 were silent to users but generated enormous console log volume (~39K lines per session).

Decision

Replace Cross-Origin-Embedder-Policy: require-corp with Cross-Origin-Embedder-Policy: credentialless on both hosting targets. Fix the related CSP directives: base-uri 'none'base-uri 'self', add 'wasm-unsafe-eval' to script-src.

Update the Bastion agent’s audit rubric to recommend credentialless as the default COEP posture, reserving require-corp for sites that control every cross-origin resource.

credentialless still provides cross-origin isolation (enabling SharedArrayBuffer, performance.measureUserAgentSpecificMemory(), and high-resolution timers) but does not require third-party servers to send CORP headers. Instead, it strips credentials (cookies, client certs) from cross-origin requests that don’t explicitly opt in — a weaker but operationally viable constraint.

Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Resource-Policy: same-origin remain unchanged. COOP protects the browsing context. CORP on our own responses prevents other sites from embedding our resources. Neither causes the third-party compatibility issues that COEP require-corp does.

Consequences

  • All five error categories are resolved. Google Tag Manager, Firebase telemetry, Firestore listeners, and WASM modules load without COEP interference.
  • Cross-origin isolation is preserved. credentialless + same-origin COOP still qualifies the page as cross-origin isolated in all major browsers.
  • The security trade-off: credentialless does not prevent a cross-origin server from reading its own response when loaded by our page — require-corp would have blocked that fetch entirely. For a site that only loads resources from Google-operated CDNs and APIs, this is not a meaningful regression.
  • Bastion’s rubric now accounts for third-party compatibility. Future audits will recommend credentialless by default and only suggest require-corp when the site controls all cross-origin endpoints.
  • The CSP base-uri change from 'none' to 'self' is a minor relaxation. It allows <base> tags pointing to the same origin, which Angular requires. It does not allow cross-origin base URI injection.
  • The wasm-unsafe-eval addition to script-src allows WebAssembly compilation without opening the door to JavaScript eval(). This is the standard CSP directive for WASM-dependent libraries.