What API devs should know about CORS
CORS: Simply; Complete.
This document and related tech talks are meant to be “just enough to fully grasp what’s happening”. Aimed primarily at API/backend developers who are sometimes confused about why they are “seeing errors about CORS”.
I’m always looking for feedback, better examples, and corrections – especially with a document as technical as this one.
I hope to provide just enough information to fully grasp the what-and-why of CORS, empowering you to “know what to google”, should this not be enough.
Hope it’s helpful!
Foundational Knowledge
- CORS is part of the Fetch API spec. It’s a way to enable read-access to cross-origin requests, which are blocked by default per the Same-origin policy.
- CORS stands for “Cross Origin– blahblahblah. The rest is less important.”
- “Same-origin policy” exists.
- “The browser’s same-origin policy blocks reading a resource from a different origin.” (cite)
- CORS is a way around it, when you need one.
- CORS doesn’t add protection. It technically removes some default restrictions. It allows one to opt-out of certain Same-origin Policy restrictions.
- CORS only applies to requests made from a (well-behaved) web-browser.
- It overrides a default browser behavior.
curl
,wget
, server-side scripts, etc, don’t have to care about CORS.
- Browsers send cookies (headers, really) along with requests on domains for which they already have them. Generally, they are not shared, even for sub-domains. (Sub-domains only if you intentionally enable it).
- These are EACH unique “Origins”:
http://example.com
http://example.com.eu
http://example.net
http://my.example.com
(less obvious; different host)http://example.com:81
(different port)https://example.com
(different scheme/protocol)https://my.www.example.com
Less foundational, but this is what it does:
- A browser might request, fetch, and even receive cross-origin content! But it will refuse to allow read access to the requesting document/script UNLESS the server of the requested document explicitly states — via CORS headers — reading that content is allowed. This is where the server-side aspect comes in.
Just one example scenario
Suppose I accidentally open a link to https://evil.example.com/. That page runs a script to automatically send a GET request from my browser to FOR EXAMPLE, https://bank.example.com/logged-in-views/my-private-info?format=json
The web browser includes a header on this request: “Origin: https://evil.example.com”
Your web browser is extremely “helpful”. Assuming you had already been logged in, it will send along your existing login cookie info in the request. (evil.example.com can’t just read other domain cookies, so it has to make a genuine, in-browser request).
When the browser receives the response from the server, the browser examines the headers on the response before determining if the requester can read it.
If the server did not send along a header called Access-Control-Allow-Origin
, it will make a safe assumption that the owners of the server do not want to share data across Origins. This is Same-origin Policy at work! So the browser — despite receiving the data — will not let the requesting document READ or access that response.
If the bank server did send an Access-Control-Allow-Origin
header on the response, the browser will compare the values provided in that header to the Origin of the document that made the request.
- If the value is “
*
” (a wild-card), the browser knows that the owners of the server want to allow ANY origin to access that response. - If the value matches the Origin (sent with the request), the browser knows that the server has done some kind of processing, and explicitly allows THIS ORIGIN access to the response: And so will allow the response to be read.
- If the value on the header doesn’t match: The browser will refuse to allow access to the response.
To paraphrase some docs:
If the resource owners at https://api.example.com
wished to restrict access to the resource to requests only from https://www.example.com
, (i.e no domain other than https://www.example.com
can access the resource in a cross-origin manner) they would send:
Access-Control-Allow-Origin: https://www.example.com
It’s up to the server
So it’s always up to the server to look at an “Origin” header, and determine if it should send back an Access-Control-Allow-Origin
header on the response. And if it should, what value does it want to supply?
To allow from a specific domain, this could be as simple as the following:
if request.headers['origin'] == 'https://trusted.example.com':
response.headers['access-control-allow-origin'] = 'https://trusted.example.com'
“When do I need to be aware of CORS?”
When you want an Origin other than your own to have browser-based access to your resource.
This all comes into play in cases where a website might be https://www.bible.com/
but needs to be able to make API Fetch calls to https://api.bible.com/
on the client side (in the browser).
Bonus content: What’s this about “preflight” requests?
The Fetch spec (that defines CORS) has a category of requests referred to as “simple requests”. Those don’t send an automatic “preflight” request. But, all others do.
To directly quote (emphasis mine) (cite):
… for “preflighted” requests the browser first sends an HTTP request using the
OPTIONS
method to the resource on the other origin, in order to determine if the actual request is safe to send. Such cross-origin requests are preflighted since they may have implications for user data.
For example: If a preflight check describing a request for DELETE bank.example.com/my-account
fails, the browser should not send that DELETE
request.
The preflight request will usually contain headers that describe the “real” request that it intends to make:
Access-Control-Request-Method
, which will contain, for example,POST
if the method of the intended request is a POST request.Access-Control-Request-Headers
, which will contain a comma separated list of “non-standard” headers that the request intends to send along. E.g., including headers likeX-Secret-API-Token
.
This preflight request gives the server a chance to confirm or deny that a given request method, set of headers, and provided Origin are allowed or disallowed.
That response to a preflight might include several headers, for example:
Access-Control-Allow-Origin
(you know this one!)Access-Control-Allow-Methods
(POST, GET, OPTIONS)Access-Control-Allow-Headers
(X-PINGOTHER, Content-Type, X-Secret-API-Token)Access-Control-Max-Age
What about XSS or CSRF?
CORS configuration allows opting out of certain protections provided by Same-origin Policy. It does not necessarily enhance security at all.
You must still protect against CSRF explicitly, especially in the case of state-changing requests. As well as considering solutions like Content Security Policy to prevent certain XSS and data injection attacks.
There’s a lot more
CORS is part of the Fetch API spec. It’s a way to enable read-access to cross-origin requests, which are blocked by default per the Same-origin policy.
There’s actually a lot more to be said about both. CORS has way more options to define more-or-less strict behavior. Some of which were hinted at with the list of Access-Control-Allow-*
headers mentioned above.
References
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
- https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
- https://fetch.spec.whatwg.org/
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- https://web.dev/strict-csp/
- https://httptoolkit.tech/will-it-cors/
- https://web.dev/same-origin-policy/
- https://enable-cors.org/index.html
- https://www.autodraw.com/