(2021-10-12) Archibald How To Win At Cors
Jake Archibald: How to win at CORS CORS (Cross-Origin Resource Sharing) is hard. It's hard because it's part of how browsers fetch stuff, and that's a set of behaviours that started with the very first web browser over thirty years ago. Since then, it's been a constant source of development; adding features, improving defaults, and papering over past mistakes without breaking too much of the web.
*Anyway, I figured I'd write down pretty much everything I know about CORS, and to make things interactive, I built an exciting new app:
The CORS playground*
Before I get to any of the 'how', I'm going to try to explain why CORS is the way it is, by looking at how it came into existence, and how it fits into other kinds of fetches.
Browsers have been able to include images from other sites for almost 30 years. You don't need the other site's permission to do this, you can just do it. And it didn't stop with images:
APIs like these let you make a request to another website and process the response in a particular way, without the other site's consent.
When you request other-site content using one of the methods above, it sends along the credentials for the other-site. And over the years that's created a colossal sackload of security issues.
This gets worse with a format like CSS, which has more capabilities, but doesn't immediately fail on parse errors.
From browser bugs to CPU exploits, these leaky resources have given us decades of problems
It's become pretty clear that the above was a mistake in the design of the web, so we no longer create APIs that can process these kinds of requests
More recently, we don't send web cookies along with the request from site-A to site-B, unless site-B has opted-in using the SameSite cookie attribute. Without cookies, the site generally returns the 'logged-out' view, without private data.
Firefox and Safari go a step further, and try to fully isolate sites, although how this works is currently pretty different between the two.
you don't want an evil page to be able to read the DOM of your banking page, so they decided that cross-frame scripting would only be allowed if both pages had the same origin.
From that point, features that granted deep visibility into a resource were limited to same-origin.
Some web features don't deal with origins, they deal with 'sites'. For instance, https://help.yourbank.com and https://profile.yourbank.com are different origins, but they're the same site. Cookies are the most common feature that operate at a site level,
But how does the browser know that https://help.yourbank.com and https://profile.yourbank.com are part of the same site, but https://yourbank.co.uk and https://jakearchibald.co.uk are different sites?
in 2007 Mozilla swapped their heuristics for a list. That list is now maintained as a separate community project known as the public suffix list, and it's used by all browsers and many other projects.
9000+ entries of the public suffix list
How could we allow those more powerful APIs to work across origins?
Unfortunately there're a lot of HTTP endpoints out there that 'secure' themselves using things other than browser credentials.
*what about accessing the raw bytes of the resource? In that case it doesn't make sense to use resource-specific metadata for the opt-in. And besides, HTTP already has a place for resource metadata…
The proposal by the Voice Browser Working Group was generalised using HTTP headers, and that became Cross-Origin Resource Sharing, or CORS.*
Most modern web features require CORS by default, such as fetch().
Unfortunately there's no easy rule for what does and doesn't require CORS.
The best way to figure it out is to try it and look at network DevTools. In Chrome and Firefox, cross-origin requests are sent with a Sec-Fetch-Mode header which will tell you if it's a CORS request or not. Unfortunately Safari hasn't implemented this yet.
If an HTML element causes a no-CORS fetch, you can use the badly-named crossorigin attribute to switch it to a CORS request.
By the time CORS was developed, the Referer header was frequently spoofed or removed by browser extensions and 'internet security' software, so a new header, Origin, was created, which provides the origin of the page that made the request.
Browsers tried adding it to regular GET requests too, but it broke a bunch of sites that assumed the presence of the Origin header means it's a CORS request
*To pass the CORS check and give the other origin access to the response, the response must include this header:
Access-Control-Allow-Origin: **
This is unnecessary complication, but if you insist on doing this, it's important again to use the right Vary header:
Vary: Origin
A lot of popular "cloud storage" hosts get this wrong. They add CORS headers conditionally, and don't include the Vary header.
Cross-origin CORS requests are made without credentials by default. However, various APIs will allow you to add the credentials back in.
The same-site rules around cookies still apply, as do the kinds of isolation we see in Firefox and Safari. But these only come into effect cross-site, not cross-origin.
If you try to make an unusual request, the browser first asks the other origin if it's ok to send it. This process is called a preflight.
Edited: | Tweet this! | Search Twitter for discussion