Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 758

Cross-Origin-Embedder-Policy: securing embedded resources: Understanding cross-origin security headers - Part 3

$
0
0

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

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 a Cross-Origin-Reousurce-Policy header (for no-cors requests) or a relevant Access-Control-Allow-Origin (ACAO) (for cors 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:

How COEP works, taken from https://web.dev/articles/why-coop-coep

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 form POST, 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 a Cross-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 or https).
    • If the CORP header is cross-origin then the embedded resource can be loaded in any embedded document.
  • cors requests must return an Access-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:

A flow chart of CORS vs CORP for COEP: require-corp

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:

image from https://developer.chrome.com/blog/coep-credentialless-origin-trial

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:

  1. If you control the resource, add a Cross-Origin-Resource-Policy header set to same-origin, same-site, or cross-origin header, depending on your requirements
  2. 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 of no-cors) by adding the cross-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 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 a Cross-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!

image from https://developer.chrome.com/blog/coep-credentialless-origin-trial

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:

A flow chart of CORS vs CORP for COEP: require-corp

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.


Viewing all articles
Browse latest Browse all 758

Trending Articles