Skip to content

CORS, CSP, CORP and COEP compared: browser security headers

CORS, CSP, CORP, COEP and COOP each answer a different question. Learn who sends each header, what it protects, and how they interact when a request fails.

The ProxifyEdge team 5 min read

Browsers enforce a handful of cross-origin security policies, and their names are maddeningly similar: CORS, CSP, CORP, COEP and COOP. When a request fails, it’s easy to fix the wrong one. This comparison of CORS, CSP, CORP and COEP explains which question each header answers, who sends it, and what breaks when it’s misconfigured, so you can tell them apart the next time something is blocked.

The one-line version

Each mechanism protects a different party, and the header lives on that party’s responses:

HeaderSent byQuestion it answersProtects
CORS (Access-Control-Allow-*)The API serving the resourceMay this other origin’s script read my response?The resource’s data
CSP (Content-Security-Policy)Your pageWhich sources may my page load from, execute or connect to?Your page, from injected content
CORP (Cross-Origin-Resource-Policy)The resourceMay other origins embed me at all, even without reading me?The resource, from side-channel leaks
COEP (Cross-Origin-Embedder-Policy)Your pageMust everything I embed opt in to being embedded?Your page’s isolation
COOP (Cross-Origin-Opener-Policy)Your pageMay cross-origin windows keep a reference to me?Your page’s browsing context

The rest of this article unpacks each row.

CORS: permission to read a cross-origin response

CORS relaxes the same-origin policy. By default, script on https://app.example.com can send a request to https://api.example.com but can’t read the response. The API grants permission with response headers:

Access-Control-Allow-Origin: https://app.example.com

The direction matters: the server that owns the data decides. Nothing your page does can grant itself access. A missing header produces the classic error covered in how to fix ‘No Access-Control-Allow-Origin header is present’, and a request with custom headers adds a preflight.

CORS applies to requests made in cors mode: fetch and XMLHttpRequest to other origins, and elements that opt in with the crossorigin attribute, such as <img crossorigin> or <script type="module">.

CSP: what your page is allowed to load

Content Security Policy is the reverse. It’s a header your page sends to restrict what the page itself may do, mainly to limit the damage of cross-site scripting:

Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self' https://api.example.com; img-src 'self' data:

The directive that overlaps with CORS is connect-src. It governs the URLs that fetch, XMLHttpRequest, WebSockets and EventSource may contact. If https://api.example.com isn’t listed, the browser blocks the request before it’s sent and logs a CSP violation:

Refused to connect to 'https://api.example.com/v1/items' because it violates the following
Content Security Policy directive: "connect-src 'self'".

That’s the key diagnostic difference. A CORS failure means the request went out and the response couldn’t be read; a CSP failure means the request never left. Both surface in your code as TypeError: Failed to fetch, so read the console message. Our guide to debugging CORS errors in DevTools shows how.

To call an API from a page with a strict CSP, you need both: the API must send CORS headers, and your CSP must list the API’s origin in connect-src.

CORP: a resource refusing to be embedded

The same-origin policy has always allowed some cross-origin loads without CORS. An <img>, <script> or <video> can load from anywhere, as a “no-cors” request; the page can display the result but not read its bytes.

“Can’t read” turned out to be weaker than it sounded. Side-channel attacks such as Spectre showed that once a response is loaded into a page’s process, the page may be able to infer its contents. Cross-Origin Resource Policy lets a resource refuse those loads entirely:

Cross-Origin-Resource-Policy: same-site

The three values:

  • same-origin: only pages on exactly this origin may load the resource.
  • same-site: pages on the same registrable domain may load it.
  • cross-origin: anyone may load it, an explicit statement that embedding is fine.

CORP is enforced for no-cors requests. For a cors request the server already controls access with Access-Control-Allow-Origin. When a response is blocked by CORP, Chrome reports net::ERR_BLOCKED_BY_RESPONSE.NotSameOrigin or a similar code in the Network panel.

COEP: requiring every embed to opt in

Cross-Origin Embedder Policy is the page-side counterpart of CORP. A page that sends it promises to load only resources that explicitly allow it:

Cross-Origin-Embedder-Policy: require-corp

With require-corp, every cross-origin subresource must either be loaded in cors mode with a valid CORS response, or carry a Cross-Origin-Resource-Policy header permitting it. Anything else is blocked. The credentialless value is a gentler alternative: no-cors cross-origin requests are sent without cookies, so they don’t need CORP.

Why would a page volunteer for this? Because COEP, together with COOP, makes the page cross-origin isolated, which unlocks powerful features browsers otherwise restrict:

  • SharedArrayBuffer, needed for multi-threaded WebAssembly.
  • High-resolution timers and performance.measureUserAgentSpecificMemory().

You can check whether a page achieved it with self.crossOriginIsolated.

COOP: cutting window references

Cross-Origin Opener Policy controls whether a page shares a browsing context group with windows it opens or that open it:

Cross-Origin-Opener-Policy: same-origin

With same-origin, a cross-origin popup or opener loses its reference to your window, so window.opener becomes null and the two can’t interact. It defends against cross-window attacks and is the other half of cross-origin isolation. A common gotcha is that same-origin breaks OAuth or payment popups that rely on window.opener. Use same-origin-allow-popups if your page opens such popups.

How they interact in practice

A few scenarios show where each one bites:

  • Your app calls a third-party JSON API. The API needs CORS. Your page’s CSP needs the API in connect-src. CORP and COEP don’t apply, since a cors fetch is governed by CORS.
  • You enable cross-origin isolation for a WebAssembly app. Every image, font and script from other origins now needs CORS or CORP, or it breaks. Third-party embeds that send neither are the usual casualties.
  • You host user uploads on a separate domain. Send Cross-Origin-Resource-Policy: same-site so other sites can’t embed them, and a restrictive CSP on that domain so an uploaded HTML file can’t run script.
  • A request fails and you’re not sure why. If the console says “Refused to connect,” it’s CSP. If it says “blocked by CORS policy,” it’s CORS. A blocked image with ERR_BLOCKED_BY_RESPONSE points to CORP or COEP.

Where a proxy fits

A CORS proxy changes the CORS half of the picture only: it returns the Access-Control-* headers that the API didn’t. Your page’s CSP still has to allow the proxy’s origin in connect-src.

With ProxifyEdge, that means adding https://api.proxifyedge.com to connect-src. Responses come back with the CORS headers your key’s origin list allows, and upstream Access-Control-* headers aren’t relayed. The headers reference lists which response headers your script can read.

Key takeaways

  • CORS is the server’s permission for your script to read its response.
  • CSP is your page’s own restriction on what it may load and connect to; connect-src blocks requests before they’re sent.
  • CORP lets a resource refuse to be embedded cross-origin, even when it can’t be read.
  • COEP makes your page accept only resources that opt in, and with COOP enables cross-origin isolation.
  • The console message names the policy that blocked a request. Fix that one, not its look-alike.

For the foundation all of these build on, read What is CORS?.

Are you sure?