CORS and the SOP explained
Introduction to Cross-Origin Resource Sharing (CORS) and the Same-Origin Policy (SOP). Structured as a dialogue, and focused on the why.
Photo by Daniel Fazio on Unsplash
For some reason, I’ve always had some trouble wrapping my head around CORS. It’s easy enough to read its specification and to implement, but it always somehow seems backwards to me. Over the years, I’ve probably thought or heard all of the things below.
- Users of my API complain that they can’t use my API because of a CORS error. Stupid CORS; how do I disable it?
- I want to call an API. How is it that the external API gets to specify whether I can call it or not? This sounds like the opposite of what should be happening!
- Why would I put thought into the list of allowed origins. After all, I know the
Originheader is easy to feign.
If you recognize any of this, read on! I wrote this story mostly for myself, to thoroughly weed out the remnants of the thoughts above from my brain, I’m of course hoping that it can be useful for you too.
The goal is not just to explain how CORS works, but also to discuss the need it addresses and why it works the way it does. This helps in developing a deeper understanding. In my opinion it can be hard to deduce such motivation from reference documentation sources. I’ve structured this post in a question-response style, in an attempt to apply the Socratic method.
What is cross-origin resource sharing (CORS)?
To explain what CORS is, you’ll need to know what the same-origin policy is. Do you?
Let’s say I don’t… What is the same-origin policy (SOP)?
The same-origin policy (SOP) is one way in which browsers protect their users from the dynamically loaded (JavaScript) application code executing when opening a webpage. The SOP specifies that dynamic requests can only be sent to URLs with the same origin as the page they are sent from.

When do two URLs have the same origin?
Two URLs have the same origin if and only if they share the same protocol, domain and port.
Another kind of origin (story).
Why does the SOP exist? What sort of attacks does it prevent?
When sending a request to a URL, a browser may send along information such as cookies to that domain. These may authorize it to perform actions. Think of posting a Medium post or sending a tweet. Even without cookies, users can be identified through other means such as their IP address or membership of a protected network. The SOP allows users to visit untrusted sites without risking those sites performing authorized requests, by preventing requests to third parties.
Untrusted sites? Sounds like you shouldn’t be visiting those in the first place!
Pretty much al sites on the Internet are “untrusted sites”. The site you’re on right now is an untrusted site. You didn’t know anything about this site before you opened it. You did not review the code. You certainly do not want it to run it with special privileges!
I see. It’s thanks to the SOP that the page that I’m reading right now cannot send tweets in my name, then.
Exactly. Some APIs shouldn’t be callable from just anywhere, especially not together with browser-kept secrets or session identifiers. The ability for JavaScript code running in the browser to call, say, the API that is driving your bank’s site: that’s just not the kind of authority you want to implicitly delegate to every site you visit.
Why does the SOP not block same-origin requests?
The SOP assumes that requests to resources with the same origin as the page you are visiting are totally fine. Which is reasonable! It’s otherwise quite restrictive though. It blocks (almost) all other dynamic requests—so-called cross-origin calls.
The SOP allows pages to call APIs sharing its origin, but blocks most cross-origin requests.
I spotted the (almost) there! Are not all cross-origin requests blocked by the SOP?
Certain cross-origin requests are not subjected to the SOP. One example are requests to dynamically load and display images on a page.
- It is often, but not always, allowed to send out or “write” cross-origin requests from application code.
- It is generally allowed to dynamically embed cross-origin content.
- It is not usually allowed to read the responses to cross-origin requests from application code.
You can read more here. The cross-origin requests that are allowed by the SOP are also known as simple requests.
If the SOP is so great, then why do we have CORS?
The SOP is an important security measure, but it’s very restrictive. Although the SOP protects users of browsers against unknowingly calling APIs, it also prevents legitimate cross-origin API usage. Given that you are reading this page, you probably have such a use case in mind.
Ok, so to recap…
- The SOP is there to prevent a user (tricked into) visiting
https://malicious.comfrom carrying out actions againsthttps://protected-api.com. - CORS is there to allow
https://api.github.comto be accessible from sites other thanhttps://api.github.comitself, while still preventing the scenario above.
CORS relaxes the SOP, by specifying how an API at origin B can broadcast allowing requests from other origins
Let’s finally get to it then. How does CORS work?
An API like https://protected-api.com can tell browsers when it’s ok for it to be called from foreign origins through CORS HTTP headers. It can specify:
- Which origins may call it—through the
Access-Control-Allow-Originheader or ACAO header. You may specify a wildcard origin; this is well-supported by browsers. - Whether to attach browser-protected information such as cookies to requests—through the
Access-Control-Allow-Credentialsheader or ACAC header. The default value is false, and the only possible value is true. - What headers may be sent along with requests—through the
Access-Control-Allow-Headersheader. You may specify the wildcard header, but this is not currently well-supported by browsers.
All requests made by browsers implementing CORS also include the origin of requests in the request’s Origin header. Servers can change the value of the response headers above as a function of request’s origin header.
There’s more CORS headers, but the three headers above are probably the most important ones; check here for a full overview of CORS headers.
Specifying which requests are allowed through plain old HTTP headers? That can’t be right; headers are part of the response to a request, and you just told me cross-origin requests are blocked!
That’s right! To set the headers above in response to a request, the request has to already be made, and we’ve seen that most cross-origin requests are blocked by the browser unless unblocked by these headers. Browsers address this by sending what’s known as a preflight request before the actual request, or in this context: preflighted request. The preflight request is an OPTIONS request specifying the preflighted request’s headers (Access-Control-Request-Method), and request method (Access-Control-Request-Headers). Like any request from a browser supporting CORS, the preflight request also includes the origin header (Origin). An API supporting CORS wanting to allow the preflighted request has to answer the preflight request by sending an empty response (code 204) with appropriate CORS headers such as the ones above.

What if you don’t answer the preflight request?
If your API does not implement CORS or allow the request through a response to the preflight request with appropriate CORS headers, then the browser falls back to the SOP, and the cross-origin request is denied. Simple as that!
Are all requests subjected to CORS and preflighted?
No. Firstly, all requests to resources at the same origin as the site you’re on are not subject to the SOP; such requests are not preflighted, and cannot be restricted through CORS. We’ve also already discussed that certain cross-origin requests, called simple requests, are not subjected to the SOP. Simple requests are not preflighted. However, the response to simple requests can and often should still include CORS headers. Without CORS headers, the browser will not permit the code that triggered the request to read the request’s response.
Can you tell me more about these simple requests?
Simple requests are indeed interesting, both because they can save you from having to make a preflight request, and in the context of cross-site request forgery (CSRF) attacks. In a nutshell, simple requests are GET, HEAD and POST requests onto which only a select set of HTTP headers may be set and for which the Content-Type has to be one of three allowed values. There’s some things you’ll want to remember about simple requests.
- Requests with
Cookie,Authorizationor custom headers are never simple requests. - Requests with content type
application/jsonare never simple requests. - POST requests can be simple requests! You may want to take this into account when you’re developing your API, especially if you authorize requests e.g. through source IP.
- The response to simple requests cannot be read unless the response includes appropriate CORS headers
Check out the MDN Web Docs for a full description of what requests are considered simple.
Simple requests are not preflighted or subjected to the SOP or CORS
When can and should I use CORS?
You now know what the SOP sets out to protect against, what kind of request CORS enables and what the most important CORS headers are. Some examples:
- Your API is not sensitive, but only meant for consumption by you your own site, hosted on the same domain as the API. There’s no need for CORS, so don’t bother.
- Your API will be called only from a CLI application. There’s no need for CORS, so don’t bother.
- Your API is not sensitive. You do not care about authorization. You want it to be available for use on other websites. You can allow requests from the wildcard origin. There’s no need for the ACAC header.
- Authorization on your API works through application-provided bearer tokens, and you want it to be publicly available. Allow the wildcard origin. Allow the
Authorizationheader by appending it to theAccess-Control-Allow-Headersheader. There’s no need for ACAC. - Authorization on your sensitive API works by verifying caller IPs or another environment-dependent identity mechanism (think of e.g. the metadata service API offered by cloud providers). You likely don’t want to enable CORS on this API, so simply don’t respond to CORS OPTIONS requests and don’t set CORS headers on responses. If you do, think twice about what origins you trust; do not set the wildcard ACAO header!
- Authorization on your API works through cookies or basic authentication, and you want to enable its usage from another origin that you also control or trust. Set the ACAC header to
true, and the ACAO header to a carefully crafted set of origins (or respond with the Origin header specified in the preflight request, but only for a select set of origins).
That’s it! That should answer pretty much everything you ever wanted to know about CORS.
But I’d really like to ask some more questions…
If CORS is there only to relax the SOP, then why do people refer to CORS as a security measure?
Weird, huh? From the point of view of an API designer or web admin, CORS is not really a security measure, but rather an enabling feature that allows relaxing the browsers’ draconian SOP. CORS does nothing to keep your server or the resources it serves secure; it protects your users from malicious code running in their browsers, but it does not protect you from malicious users. Non-browser clients and insecure or malicious browsers simply do not implement CORS. The origin they send can be wrong. CORS is there only to support the common situation where the browser can be trusted (to correctly implement CORS), but the code running in it cannot be trusted.
CORS is not related to security then?
Well, as mentioned, SOP+CORS does protect your users from malicious code running in their browsers. Configuring CORS correctly determines how secure it is to use your API from browsers. Assume for a second that you’re a programmer for Twitter. If you’d allow any origin to call the Twitter API together with the Twitter cookies stored in the browser, then you’d certainly have undermined the security of the Twitter API. You just made Twitter extremely vulnerable to cross-site request forgery attacks (CSRF).
How does CORS prevent data being sent to malicious destinations?
It does not. Data that is accessible to code running in the browser can be sent to a malicious server implementing CORS. All it takes is for the receiving server to be configured to accept data from all sources or your specific origin. Preventing data from getting out from the browser is simply not what CORS sets out to do.
So CORS makes browsing more insecure & enables data exfiltration?
Not really. Even without CORS, data can get out in the form of a form-encoded “simple” POST request. There are also sneaky ways to set up bidirectional communication even when using an unrestricted SOP, e.g. by reading properties of embedded content.
Is it useful to also check CORS headers in the API implementation?
Although it might first seem like checking the Origin header in your API implementation would increase security, it actually really doesn’t. As discussed above, CORS really only makes sense within the context of a trusted browser. Malicious clients can simply set the Origin header to whatever they want.
What does the ACAC header actually do?
The ACAC header specifies whether to allow credentialed requests. Credentialed requests include credentials—private data known to the browser but not the application code triggering the request, such as cookie data or TLS client certificates. To send a credentialed cross-origin request, the ACAC header needs to allow it, but the application code also needs to request a credentialed request.
Can I set the Authorization header without the ACAC header?
Setting the Authentication header from the JavaScript application code does not require the ACAC header to be set. This is actually quite common on single-page applications. You can embed an authorization header into the request just like any other header. Embedding the Authorization header does trigger a preflight request, and will only work if Authorization is part of the server’s Access-Control-Allow-Headers response header. The one way in which the Authorization header is special is that it is not matched by a wildcard value for Access-Control-Allow-Headers .
Why can’t I combine the ACAC header with for wildcard origins?
You may recognize the following error:
Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ’*’
Although the error is quite descriptive, it does not explain why this combination is not allowed. If you think about it, such a server configuration allows any code, from any site, to make a request to your API using the credentials (session info) cached by your browser. If, say, Facebook, would set up their service this way, then any website on the internet could simply post messages to your Facebook wall. There’s simply no reason why you’d ever want to use this combination of settings.
- If you don’t use credentials (basic authentication, session cookies, client TLS certificates) then the combination of wildcard ACAO with ACAC header may not be disastrous, but then the ACAC header would simply be completely redundant.
- It make sense to combine a wildcard ACAO with an
Authorizationheader set from the application code, but this scenario does not require the ACAC header.
What is origin reflection?
Origin reflection refers to the practice of re-playing the Origin header of a preflight request into the ACAC header of the preflight response. I encountered origin reflection a number of times as a way to circumvent the restriction on the combination of wildcarded ACAC header and ACAC header. This is almost certainly a very bad idea…
Is origin reflection always a bad idea?
Origin reflection is not a bad idea per se, as it may prevent you from having to return a long list of trusted origins in the ACAO header; just make sure to only reflect requests from trusted origins.
CORS is causing me grief. Can I disable CORS in my browser?
Yes, you usually can. However, disabling CORS will likely replace the CORS errors you are seeing with SOP errors.
Can I disable the SOP then?
Yes, you can. E.g. you can start Chrome with the startup option --disable-web-security. However, this leaves you quite vulnerable on the Internet, as it will expose you to the attack we discussed before. Any website you visit will be able to post embarrassing stuff on your Facebook wall! Also, keep in mind that you can only disable SOP/CORS on your own machine, not other people’s computers. Disabling SOP/CORS locally won’t enable the use of your API for others. The only time when disabling SOP/CORS may be useful is during development, when you want to test an API from a “development origin” that is not allowed by the API.

I’m developing an app and hosting it on a temporary development origin that is not allowed by the API that I am calling. Is there a way to disable SOP+CORS more selectively?
The SOP applies when you run code from untrusted origins, which by default all sites loaded from the Internet. Requests from trusted origins, which often includes localhost by default, are not subjected to the SOP; they can fetch resources, credentialed and not credentialed alike, from any location. You can usually set up your browser to treat your development origin as a trusted origin.
Are there any caveats I should be aware of?
Using a deprecated but still universally supported mechanism, application code running in the browser can dynamically change the domain used for the purposes of SOP and CORS checking to a superdomain of their actual domain. Therefore, if you allow an origin, you should assume that all subdomains of that origin will be able to call your API too.
Does the SOP pre-date CORS?
Indeed. CORS has only been accepted as a W3C recommendation since 2014, while the SOP was introduced pretty much together with JavaScript by Netscape in 1995. Before the advent of CORS, dynamic cross-origin “back-channel” requests used to simply not be possible. This is actually also reflected in a lot of protocols and legacy application flows, such as the implicit flow in OAuth2.0, which is there to support browsers only supporting cross-origin information sharing through redirects.
CORS is awesome!
I think this feeling is shared by many developers that were in web development before CORS exited. Newer developers likely only know CORS from CORS errors and misconfiguration; they don’t seem to like it as much.
I’m glad to have gotten you to this insight. All on your own ;-).