Cross-Origin-Embedder-Policy (COEP) is one of three browser headers used to isolate a site from cross-origin attacks, the other two being Cross-Origin-Opener-Policy (COOP) and Cross-Origin-Resource-Policy (CORP). COOP protects your browsing context from malicious popups, and CORP stops other sites from embedding your resources without permission. COEP works in the opposite direction: it controls what your own site is allowed to pull in from other origins. Get it wrong and you either break real functionality or leave a genuine opening for cross-site script inclusion (XSSI) and Spectre-style side-channel attacks.
The problem with embedding cross-origin resources
Browsers allow several HTML elements to load cross-origin content without any same-origin check. Images referenced in an img tag, media in video and audio tags, scripts loaded through script src, and stylesheets linked with link rel=”stylesheet” are all fetched this way by default. This kind of request is called a no-cors request, and browsers have permitted it for decades because it is how the web has always worked.
Attackers use this openness for XSSI attacks. They embed a resource from a target site inside their own page and infer sensitive information from side effects such as image dimensions, script execution timing, or error behaviour. It gets worse because no-cors requests carry cookies by default. If an attacker can force your browser to make a no-cors request to a bank or internal tool, that request goes out fully authenticated.
What Cross-Origin-Embedder-Policy actually controls
The COEP header can be set to one of three values, and each changes how strictly your site checks the resources it embeds.
- unsafe-none is the default. It adds no extra protection and behaves exactly like a site with no COEP header at all.
- require-corp is the strictest option. Every cross-origin no-cors request must come back with a Cross-Origin-Resource-Policy header that explicitly allows it, and every cors request must come back with a valid Access-Control-Allow-Origin header.
- credentialless is a middle ground. no-cors requests are still allowed even without a CORP header, but they are sent without cookies, and any cookies the response tries to set are discarded.

cors vs no-cors requests, and why COEP cares about the difference
Cross-origin no-cors requests are the default and are usually allowed without any negotiation. An image tag pointing at another domain is a classic example.
<img src="https://some-other-domain.com/photo.jpg" />
This request goes out as a plain no-cors request. If you covered CORP in an earlier read, you will recall that CORP adds protection specifically to this kind of request by restricting which sites are allowed to load the resource. On its own, this markup tells the browser nothing about whether the embedding is safe, it simply fetches the image.
You can turn the same request into a cors request by adding the crossorigin attribute.
<img src="https://some-other-domain.com/photo.jpg" crossorigin />
With this attribute in place, the browser now requires the server to return an Access-Control-Allow-Origin header before it will use the response. This is the same negotiation used by fetch() calls to other origins, and it is a stricter, more explicit handshake than a plain no-cors request.
How require-corp ties CORP, CORS and COEP together
Once you set Cross-Origin-Embedder-Policy to require-corp, the rules become strict and unforgiving. A no-cors request must return a Cross-Origin-Resource-Policy header, and its value decides who gets access. A value of same-origin means only requests from the exact same origin can load the resource. A value of same-site means any subdomain on the same registrable domain and scheme can load it. A value of cross-origin means it can be embedded anywhere. A cors request, on the other hand, must return a matching Access-Control-Allow-Origin header instead.
A common mistake is assuming these two mechanisms are interchangeable. They are not. Returning Access-Control-Allow-Origin on a response to a no-cors request does not satisfy require-corp, and returning a Cross-Origin-Resource-Policy header on a response to a cors request does not satisfy it either. Each request type has to be answered with the header it actually expects.


Why bother: unlocking SharedArrayBuffer through cross-origin isolation
The concrete reason most teams end up dealing with COEP is that certain browser APIs are locked behind a state called cross-origin isolation. SharedArrayBuffer, performance.measureUserAgentSpecificMemory(), and high-precision timers with finer resolution are only available once your page is isolated, a restriction browsers added after the Spectre disclosures to limit side-channel attacks. To reach this state you need two headers set together: Cross-Origin-Embedder-Policy: require-corp and Cross-Origin-Opener-Policy: same-origin.
You can confirm whether isolation actually took effect by checking window.crossOriginIsolated at runtime, rather than assuming your header configuration worked.
const myWorker = new Worker("worker.js");
if (window.crossOriginIsolated) {
// site is isolated, so can use SharedArrayBuffer
const buffer = new SharedArrayBuffer(16);
myWorker.postMessage(buffer);
} else {
// site is not isolated, cannot use SharedArrayBuffer
const buffer = new ArrayBuffer(16);
myWorker.postMessage(buffer);
}
This pattern matters in practice because a missing or misconfigured header does not throw an error anywhere obvious. Your code simply falls into the else branch, and features like SharedArrayBuffer quietly become unavailable, which is a frustrating thing to debug after the fact if you never checked the flag directly. Even when you do not need these APIs, enabling isolation is a reasonable hardening step on its own, though getting there cleanly is often the hard part, which the next section covers.
credentialless: a practical middle ground
The original two-value version of COEP created a real rollout problem. Before switching on require-corp, every single embedded resource on your page needs a CORP or CORS header, including third-party resources you may not control at all. If a third party will not add the header you need, you are simply stuck and cannot enable require-corp safely.
The credentialless value was added to work around exactly this gap. When you use it, no-cors requests still work without needing a CORP header, but they are sent without credentials, meaning no cookies go out and any cookies the response tries to set are discarded. Because the resource is fetched anonymously, it is treated as public content and considered safe to embed without an explicit opt-in from the third party.


The trade-off worth flagging before you rely on this in production is browser support. credentialless works well in Chromium-based browsers and in Firefox, but Safari does not support it at all. Treat it as an easier path to isolation on some browsers rather than a universal fix, and test your fallback behaviour on WebKit before you ship anything that depends on it.
Rolling out COEP without breaking production
Enabling COEP directly in enforcing mode is risky, even with credentialless available, because you cannot always predict which embedded resource is going to fail the check. The safer approach is to use the Cross-Origin-Embedder-Policy-Report-Only header first. It simulates the policy you plan to enforce, whether that is require-corp or credentialless, and reports violations through the Reporting API without actually blocking anything.
Run in report-only mode for a while, review what gets flagged, fix the resources that need CORP or CORS headers, and only then switch to the enforcing header. This staged rollout catches the resources you forgot about, such as an analytics script or a font served from a CDN, before real users hit a broken page.
Practical takeaway for teams running Blazor WebAssembly
If your team is running multithreaded WebAssembly in Blazor, which depends on SharedArrayBuffer, this stops being a theoretical browser security topic and becomes a deployment requirement. Without both isolation headers set correctly at the server, gateway, or CDN layer, the runtime typically falls back to single-threaded execution instead of throwing a clear error, so check window.crossOriginIsolated explicitly in each browser you support rather than assuming your header configuration is correct.
Setting the two headers from ASP.NET Core middleware is the easy part. The harder, more operational part is getting every third-party resource your app embeds, fonts, analytics scripts, CDN-hosted assets, to cooperate with require-corp or to work cleanly under credentialless. Testing with the report-only header before enforcing anything is worth the extra week it takes, especially on a production app with an established set of third-party dependencies.
Leave a Reply