
In this post I discuss the Cross-Origin-Embedder-Policy
(COEP) header. I describe why you need it, how it protects your site, and how it unlocks additional features as part of the cross-origin isolation requirements. Along the way I describe the difference between cors
and no-cors
requests when embedding resources in your site, and how the COEP header interacts with the Cross-Origin-Resource-Policy
(CORP) header and cross-origin resource sharing (CORS) features.
- Isolating your site with
Cross-Origin-Embedder-Policy
- Understanding the relationship between COEP, CORP, and CORS
- Accessing
SharedArrayBuffer
by enabling cross-origin isolation - Easier isolation with
credentialless
- Rolling-out
Cross-Origin-Embedder-Policy
safely with reporting
Isolating your site with Cross-Origin-Embedder-Policy
In my previous post on the Cross-Origin-Resource-Policy
(CORP) I explained that the same-origin policy doesn't apply to most cases where the requests are made by embedding links in a document. For example browsers will allow cross-origin requests by default for
- Images linked in
<img>
elements. - Media linked in
<video>
and<audio>
elements. - JavaScript files referenced in
<script src="…"></script>
. - CSS files referenced in
<link rel="stylesheet" href="…">
(as long as the stylesheet has the correct MIME type).
Malicious sites can use this fact to exploit a class of vulnerabilities termed Cross Site Script Inclusion (XSSI). XSSI attacks embedded a resource from your site inside an attacker's site, which can then leak sensitive data from your site to the attacker. That's on top of speculative side-channel attacks like Spectre which rely on cross-origin access.
The Cross-Origin-Resource-Policy
header protects you in this scenario, as it prevents your resources from being loaded by the attacker website unless you explicitly declare that it's safe to do so.
What's more, cross-origin requests like these even include cookies. That means if an attacker manages to force you to embed an <img>
to a third-party site (for example), they can make credentialled requests to that site.
While CORP protects you from being embedded, the Cross-Origin-Embedder-Policy
(COEP) protects your site as the embedder. By adding this header you can declare that your site should only load resources that have explicitly opted-in to being loaded across origins.
The Cross-Origin-Embedder-Policy
header can be set to one of three values:
unsafe-none
—the default value, doesn't add any additional projections.require-corp
—the strictest value, requires that all cross-origin requests either include aCross-Origin-Reousurce-Policy
header (forno-cors
requests) or a relevantAccess-Control-Allow-Origin
(ACAO) (forcors
requests). More on that later!credentialless
—a half-way value,no-cors
cross-origin requests are sent without cookies and no cookies are saved from the response, but there are no requirements for CORP or ACAO headers.
If you're anything like me, then you should be thoroughly confused😅 I've seen the following image (from https://web.dev/articles/why-coop-coep) used to try to explain COEP, and maybe it will resonate:
Unfortunately, I think the image is overly simplistic, and assumes a lot of understanding of how Cross Origin Resource Sharing (CORS) and same-origin policy work. In the next section I'll try to be more explicit about the relationship between COEP, CORP, and CORS.
Understanding the relationship between COEP, CORP, and CORS
The first thing to understand is the difference between cors
and no-cors
requests. Cross-origin no-cors
requests are typically allowed without issue. The examples I described previously, such as links in an <img>
element are no-cors
requests:
<img src="https://some-other-comain.com/photo.jpg" />
If you read my previous post on the Cross-Origin-Resource-Policy
(CORP) header, you may remember that CORP adds extra protection to resources fetched using no-cors
requests, by enforcing that only same-origin
or same-site
resources can be accessed by a document.
In contrast, if you're making cross-origin requests using the fetch()
API, these will typically be cors
requests. These require that the server opts-in to cross-origin access by returning the Access-Control-Allow-Origin
header with an appropriate value. Returning the Access-Control-Allow-Origin
header opts the site in to cross origin resource sharing (CORS).
There are additional pre-flight requirements for "complex" HTTP calls that are anything other than a simple
GET
or formPOST
, plus additional requirements if you want to send cookies. I'm not going to go into more details on CORS here; I've discussed CORS in the past and there's plenty of resources around if you want to learn more!
You can also turn a no-cors
embedded request into a cors
request by adding the crossorigin
attribute, for example:
<img src="https://some-other-comain.com/photo.jpg" crossorigin/>
The Cross-Origin-Embedder-Policy
header ties everything all together. If you set the COEP value to require-corp
, then
no-cors
requests must return aCross-Origin-Resource-Policy
header value, and it must indicate that the resource is allowed to be accessed. Based on the CORP header value:- If the CORP header is
same-origin
then the embedded resource can only be loaded if it has the same origin as the COEP document. - If the CORP header is
same-site
then the embedded resource can only be loaded if it has the same site as the COEP document, i.e. it has the same domain (andrewlock.net
) and scheme (http
orhttps
). - If the CORP header is
cross-origin
then the embedded resource can be loaded in any embedded document.
- If the CORP header is
cors
requests must return anAccess-Control-Allow-Origin
header (i.e. a CORS header) value that indicates the resource may be accessed.
We can alternatively visualize the various paths here as a flow chart:
An important thing to understand is that returning an Access-Control-Allow-Origin
CORS header when the request is a no-cors
request is not sufficient to satisfy a COEP header of require-corp
. Similarly, a cors
request with a CORP header will not satisfy the embedded policy.
If we consider just the no-cors
case for now, then the following image from the chrome developer blog demonstrates that with an embedder policy of require-corp
, only responses that include a CORP header value of cross-origin
will be allowed through:
There's a lot of moving pieces here, so you might well be wondering: why bother?
Accessing SharedArrayBuffer
by enabling cross-origin isolation
I motivated our initial discussion of the CORP and COEP headers by talking about XSSI attacks, as well as speculative side-channel attacks like Spectre, which rely on cross-origin access. But a concrete reason you may have to consider these headers is if you want to use certain features like SharedArrayBuffer
, performance.measureUserAgentSpecificMemory()
or high-precision timers with better resolution.
These features are only available when your site is deemed to be in an "isolated" state from other origins, to ensure it's not open to side-channel attacks. To enable cross-origin isolation, you must set two response headers:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
In this state, cross-origin isolation is enabled, and you can use features like SharedArrayBuffer
. You can check whether your site is correctly isolated by checking the window.crossOriginIsolated
property:
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);
}
Even if you don't want to use any of these capabilities, enabling cross-origin isolation is a good choice for security. The only difficulty is that it can be hard to achieve in practice!
Easier isolation with credentialless
When Cross-Origin-Embedder-Policy
was originally defined there were only two possible values:
unsafe-none
require-corp
However using require-corp
is very restrictive. This article describes the multi-step process for preparing your site to enable COEP. The big problem is that before you can enable COEP on your site, you need to update the response headers for all the embedded resources your site loads. That's fine if you control how those resources are delivered, but it's particularly problematic as you might not control cross-origin resources!
The suggested approach for preparing for require-corp
is:
- If you control the resource, add a
Cross-Origin-Resource-Policy
header set tosame-origin
,same-site
, orcross-origin
header, depending on your requirements - If you don't control the resource
- If the resource is sent with
cross-origin
CORP header, you're OK. - If the resource is not sent with a CORP header, try making a
cors
request (instead ofno-cors
) by adding thecross-origin
attribute (as described previously)- If the resource is CORS enabled (using
Access-Control-Allow-Origin
) then you're OK. - If not…there's nothing you can do 😢
- If the resource is CORS enabled (using
- If the resource is sent with
If you end up at the final bullet point, you're completely stuck. There's no way for you to safely enable require-corp
on your site unless someone else enabled CORP or CORS for the resource you're embedding.
To work around this fundamental problem, the Cross-Origin-Embedder-Policy: credentialless
value was added. When you use credentialless
you get the best of both worlds:
- Your site is still considered "cross-origin isolated", so you can use the additional features like
SharedArrayBuffer
no-cors
requests don't need to have aCross-Origin-Resource-Policy
to be embedded in your site.
Based on that description you would be right to be a little confused: how does credentialless
provide any protection?
The difference is that when you use credentialless
, no-cors
requests are sent without any credentials (i.e. cookies in most cases), and any cookies in the response are discarded. By definition, that means the resources you're embedding are available in "public", and so should be safe to embed. There's no risk of attackers being able to make the canonical "request to your bank" attack, because no credentials will be included!
If the embedded resource does return a Cross-Origin-Resource-Policy
header, it will still be honoured, but if you use credentialless
, then you will no longer be blocked from embedding a resource that doesn't add the CORP header. Compare the following image (from the Chrome developer blog), to the require-corp
version I saw previously, and you'll see that the "nothing" case is now unblocked!
If you compare the following credentialless
flow chart to the previous require-corp
version, you can see that it's only the no-cors
"CORP missing" case that has changed:
The credentialless
header means you no longer need to ask third-parties to add CORP or CORS headers just so that you can enable COEP, which should make it easier to roll out. The credentialless
option has been available for a couple of years now, and has pretty good support, but unfortunately no support on Safari means that it's not a complete solution😢.
Rolling-out Cross-Origin-Embedder-Policy
safely with reporting
Even with credentialless
, rolling out support for COEP is a potentially dangerous affair. Consequently, it's strongly recommended that you use the Reporting API built into browsers to identify any issues.
You should also consider running the Cross-Origin-Embedder-Policy
header in "report only" mode, by using the Cross-Origin-Embedder-Policy-Report-Only
header instead. This header "simulates" the provided policy, whether it's require-corp
or credentialless
, and reports violations using the Reporting API.
By using this staged approach you can identify and fix any potential cross-origin issues before you start enforcing the embedder policy on your site for real.
Summary
In this post I described the Cross-Origin-Embedder-Policy
header. I explained that it adds additional requirements to no-cors
requests that your site makes, such as for the links used in <img>
tags. I explained how it interacts with the Cross-Origin-Resource-Policy
and CORS headers depending on the request being made, and how you can use the credentialless
option to make it easier to deploy the COEP header to your site.