CORP vs COEP, two sides of cross-origin isolation
CentralCSP Team ·
Last update:
These two look similar and get mixed up constantly, but they sit on opposite ends of the same request. Cross-Origin-Resource-Policy (CORP) is something a resource sends about itself. Cross-Origin-Embedder-Policy (COEP) is something the embedding page sends about what it will accept. Once you know which side you are on, the confusion clears up.
The short version: CORP is a response header a resource sets to declare who may load it. COEP is a header the embedding page sets to declare that it only loads resources that opted in. They are two halves of the same mechanism, not competitors, and they are designed to be used together.
CORP vs COEP, the one-line difference
CORP is set by the resource and answers "who may load me." COEP is set by the page and says "I only load resources that opted in."
Everything else follows from that. A CDN, an image host, a font server, each of those is a resource, so CORP is theirs to send. The page pulling those resources in is the embedder, so COEP is the embedder's to send. The resource declares its own reachability; the page declares its own admission rule.
Cross-Origin-Resource-Policy, set by the resource
CORP is a response header the browser reads when another origin tries to load your
resource through a no-cors request (a plain <img>, <script>, <audio>, or a
stylesheet without crossorigin). It was first proposed back in 2012 as a header
called From-Origin, then revived in 2018 after Spectre made cross-origin data in
the wrong process a real leak. It has exactly three values.
Cross-Origin-Resource-Policy: same-originsame-origin: only the exact same origin (scheme, host, and port) gets the resource. The default-deny choice.same-site: the same registrable domain, so subdomains are allowed. This is what acdn.example.comservingwww.example.comneeds;same-originwould break it. One caveat:same-siteon an HTTPS resource does not admit a plain HTTP page from the same site, so a mixed-scheme setup still fails the check.cross-origin: any origin may load it. This exists mainly so genuinely public assets stay loadable, including by COEP-isolated pages.
When the header is absent, the browser behaves as if cross-origin were set, so
any site can embed your asset. Setting a value is opting out of that. If the
browser sees a stricter value than the request allows, the load fails as a network
error. Note that the request is still sent and the server still answers; the
browser discards the response body instead of handing it to the page. In Chromium
the failure shows up in DevTools as net::ERR_BLOCKED_BY_RESPONSE.
CORP protects against a few distinct things. It keeps your resource out of an attacker page's process, which is the Spectre-class side-channel motivation it was revived for. It blocks cross-site script inclusion (XSSI). And in browsers that enforce it, which is the vast majority, it prevents your assets from being displayed on other sites. That is embedding control, not bandwidth protection: the bytes still transfer before the browser strips them, and non-browser clients ignore the header entirely.
Cross-Origin-Embedder-Policy, set by the page
COEP is the other half. A page sends require-corp to say it will only load a
cross-origin subresource if that resource explicitly opted in.
Cross-Origin-Embedder-Policy: require-corpHere is the part that trips people up. Under require-corp, a missing or
malformed CORP header is treated as same-origin. A cross-origin asset that
sends no CORP at all defaults to same-origin from the embedder's point of view,
so the page cannot load it. That default is exactly what blocks unlabeled
third-party assets once you turn COEP on. Chromium even tracks it as its own
block reason, distinct from a plain CORP mismatch: the console shows
ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep. A resource opts in
either by sending Cross-Origin-Resource-Policy: cross-origin or by being
fetched through a real CORS request (the crossorigin attribute on the tag).
COEP also applies to iframes, beyond plain subresources. Under require-corp, an
embedded cross-origin document has to send a compatible COEP header of its own,
or the frame is blocked. CORP on the frame's response is not enough; the framed
document must state its own embedder policy.
How they combine for cross-origin isolation
The reason to care about both at once is cross-origin isolation. When a page sets
Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp together, the browser marks it cross-origin isolated. That state,
readable in JavaScript as self.crossOriginIsolated, is the gate for
SharedArrayBuffer and unthrottled high-resolution timers. COOP
handles the window side (it puts the page in its own browsing context group and severs
window.opener links to cross-origin pages), and COEP handles the subresource
side. Both are required.
This gate is also why most sites that send CORP started doing so. Browsers
disabled SharedArrayBuffer at the start of 2018 after Spectre. Firefox brought
it back in 2020, gated behind cross-origin isolation; Chrome re-enabled it on
desktop earlier without that gate, then required isolation on every platform in
2021.
Anything that needs shared memory today (wasm threads, ffmpeg.wasm, in-browser
video editors) must send COEP, and COEP in turn forces every cross-origin asset
the page loads to carry CORP or pass CORS. You cannot reach the isolated state
if your own assets and your CDN stay silent. The
COOP and COEP guide to cross-origin isolation
walks through the full setup.
The Access-Control-Allow-Origin gotcha
A common wrong fix is to add Access-Control-Allow-Origin to a resource and
expect require-corp to be satisfied. It is not. Sending
Access-Control-Allow-Origin on a no-cors response does nothing for COEP. To
satisfy require-corp the element has to issue a real CORS request (the
crossorigin attribute), or the resource has to send a CORP header. A CORS
response header on a request that was never made in CORS mode is ignored.
credentialless, the lower-friction option
Cross-Origin-Embedder-Policy: credentialless is a looser alternative. Instead
of requiring every cross-origin resource to send CORP, it strips credentials (no
cookies, no client certificates) from cross-origin no-cors subresource requests,
so the responses cannot be personalized data worth protecting. It still reaches
cross-origin isolation. Two limits: it only changes no-cors subresources, so
CORS-mode requests follow the require-corp rules and cross-origin iframes still
need their own COEP header. And support is partial: credentialless works in
Chromium browsers and Firefox but not Safari, so require-corp remains the
interoperable choice.
Test it with Report-Only before you enforce
COEP has a dry-run mode. Send Cross-Origin-Embedder-Policy-Report-Only: require-corp with a report-to parameter pointing at a
Reporting-Endpoints
endpoint, and the browser emits a coep report
for every load that enforcement would have blocked, without blocking anything.
The report body tells you whether the failure was a corp mismatch, a
navigation, or a worker initialization, plus the blocked URL. Run it for a
while, fix the flagged resources, then flip to the enforcing header. CentralCSP
ingests these reports alongside CSP violations, so
you see every would-be-blocked asset across real traffic before users do.
CORP vs COEP side by side
| Cross-Origin-Resource-Policy | Cross-Origin-Embedder-Policy | |
|---|---|---|
| Who sets it | The resource (image, script, font, media) | The embedding page |
| What it declares | Who may load me | I only load resources that opted in |
| Values | same-origin, same-site, cross-origin | require-corp, credentialless |
| Default when absent | Treated as cross-origin, anyone can embed (but flips to same-origin under an embedder's require-corp) | No policy, cross-origin isolation off |
| Failure mode | Blocked load is a network error, body never delivered | Non-opted-in subresources and frames are blocked |
Recommendation
Treat the two together. Set CORP on your own resources at the level they need:
same-origin for private assets, same-site where subdomains consume them, and
cross-origin only on assets that are genuinely public. The OWASP HTTP headers
cheat sheet recommends same-site as the general baseline, with require-corp
on pages. When you turn on require-corp, make sure every asset that page loads,
your own files and your CDN's, sends a CORP header the embedder will accept, or
is fetched with crossorigin. Reach for credentialless when changing every
third party is not practical and you do not need Safari, and use the Report-Only
header to find breakage before enforcing.
A scanner is the fast way to see which of your responses actually carry CORP and which pages assert COEP. The security headers scanner checks both across your site so you can spot the gaps before an isolated page starts failing to load assets.
FAQ
Do I need CORP if I already have COEP?
Yes, they cover opposite directions. COEP on your page does nothing to protect
your own resources from being loaded by other sites; only CORP on those responses
does that. And once your page sends COEP: require-corp, every cross-origin
asset it loads, including your own CDN subdomain, must send CORP or be fetched
with CORS, because a missing CORP header is treated as same-origin and blocked.
Does COEP require-corp block images?
Yes. A plain <img> pointing at a cross-origin host is a no-cors load, and under
require-corp it is blocked (Chromium shows net::ERR_BLOCKED_BY_RESPONSE)
unless the image server sends Cross-Origin-Resource-Policy: cross-origin (or a
matching same-site), or the tag uses crossorigin and the server answers with
valid CORS headers. If you control neither, proxy the image through your own
origin or use COEP: credentialless.
What is the difference between CORP and CORS?
They move in opposite directions. CORS is a server opt-in that grants cross-origin
read access to responses for requests made in CORS mode. CORP is a server
restriction that removes the default embeddability of no-cors loads (images,
scripts, media that any page could historically include). CORP only affects
no-cors requests, and CORS headers on a no-cors response are ignored, which is
why Access-Control-Allow-Origin alone never satisfies require-corp.
Should I use credentialless instead of require-corp?
A COEP value that reaches cross-origin isolation without requiring CORP from
every third party. Cross-origin no-cors subresource requests are sent without
credentials (no cookies, no client certificates), so the responses cannot carry
personalized data. CORS-mode requests still follow require-corp rules, and
cross-origin iframes still need their own COEP header. It works in Chromium
browsers and Firefox but not Safari.
Related reading
- COOP and COEP, cross-origin isolation explained
- Cross-Origin-Opener-Policy reference
- Cross-Origin-Resource-Policy reference
- Cross-Origin-Embedder-Policy reference
- The coep report type
Sources
- WHATWG Fetch, Cross-Origin-Resource-Policy header
- WICG, COEP credentialless specification
- Chromium, CORP enforcement source
- Chromium, blocked-by-response reasons
- MDN, Cross-Origin-Resource-Policy header
- MDN, Cross-Origin Resource Policy guide
- MDN, Cross-Origin-Embedder-Policy header
- MDN, SharedArrayBuffer
- web.dev, Making your website cross-origin isolated
- Chrome developers, SharedArrayBuffer updates
- OWASP, HTTP headers cheat sheet