loading...

What you should know about CORS

nicolus profile image Nicolas Bailly Updated on ・6 min read

If you're anything like me, the first time you encountered CORS (or Cross-origin resource sharing), all you wanted was for your server to accept those darn ajax requests and be done with it. So you went to stack overflow, copy pasted a code snippet to set some headers, and it worked.

There are however a few things you might want to know.

What CORS is, and what it is not

This is often a source of confusion for newcomers because it's not immediately apparent what CORS is supposed achieve. Firstly CORS is not a security measure in itself, it's actually the opposite: CORS is a way to circumvent the "Same Origin Policy" which is the security measure preventing you from making ajax requests to a different domain.

The Same Origin Policy states that a website on one domain cannot make an xhr request to another domain. This prevents a malicious website from making requests to a known website (think Facebook or Google) hoping the user are already logged-in so that it can impersonate them.
This policy is implemented by the browser (all of them implement SOP, although there are minor differences), which means that it won't apply to requests made from a server, or any other HTTP clients like cURL or POSTman. Also the server has absolutely no control over this : it will process every request as if it was coming from a trusted domain, it is entirely up to the browser to block the requests.

SOP is in no way meant to prevent an attacker from making requests to your server (since an attacker would obviously not use a browser). It's only meant to prevent legitimate users using a reputable browser from unknowingly making requests to your website.

Now CORS are a way to bypass SOP in some cases where you want to allow one specific website to make requests to your server, even though it would normally be blocked. (typically, to allow your front-end app to make requests to your API).

How CORS work.

CORS, like the rest of HTTP is basically a dialogue between the browser and the server. Assuming your front-end is on domain-a.com and your API on Domain-b.com, it would go something like this :

  -Browser : "Hey Domain-B, this script on Domain-A.com is asking me to make an ajax query to you, but I'm supposed to block it unless you tell me it's OK."
  -Server : "I don't know, but I can tell you that only https://domain-a.com is allowed to make GET, POST, OPTIONS and DELETE requests, and this needs to be validated every 10 minutes.
  Browser thinks to himself "yeah, that's the right domain, I'll send the request !"
  -Browser : "Hey domain-b, I'd like to POST on this endpoint please.
  -Server : Sure thing, here's a 200

Or if the user is on a different domain the dialogue would be shorter, and look like that :

  -Browser : "Hey domain-b.com, this script on malicious-domain.com is asking me to make an ajax query to you, but I'm supposed to block it unless you tell me it's OK."
  -Server : "I don't know, but I can tell you that only https://domain-a.com is allowed to make GET, POST, OPTIONS and DELETE requests, and this needs to be validated every 10 minutes.
  Browser thinks to himself "Oh it's not the right domain, we'd better not make that request" and proceeds to send an error in the console.

How it looks in the browser

In my little scenes above, the first question from the browser is called a Preflight request, and the corresponding HTTP verb is OPTIONS. The server should always answer to Preflight Requests with a 200 response that has no body but contains the Access-Control-Allow-Origin and a few other headers. In our example the headers would be :

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://domain-a.com
Access-Control-Allow-Methods: GET, POST, OPTIONS, DELETE
Access-Control-Max-Age: 3600

It tells the browser that it can only perform the request if it comes from domain-A.com, that it can only make GET, POST, OPTIONS or DELETE requests (a PUT request would be blocked for example), and that it can cache this information for 3600 seconds so it doesn't need to make a new OPTIONS request every single time.

And of course, if we're on a different domain that won't work. The browser will send an OPTIONS request, throw an error in the console and never send the POST request :

Pretty straight forward, right ?

Well yes, except there are a few gotchas...

Tricky things about CORS

All Responses should contain the CORS headers

You might think that if your server answers to the OPTIONS request with a 200 and the right headers you're in the clear, and you'll see the browser send the OPTIONS request, then send your actual request, and then fail miserably... That's because every request (GET, POST, or otherwise) should also contain the same "Access-Control-Allow-Headers".

Not all requests will trigger a Preflight Request

There are a few requests that won't trigger a Preflight Request, for example GET requests, or POST request with with a Content-Type header set to application/x-www-form-urlencoded. Those are "simple requests" that have always been allowed by browsers (you've always been able to make a link or POST a form to a different website, even before CORS exsisted), and you can find the full list here.
in the case of a POST request the result is a little counter-intuitive : The browser will make the POST Request (so your server will likely persist some data), and then ignore the response !

In a traditional web app you'd use application/json as a content-type so there will be a preflight request, but keep in mind your server may still receive POST requests from other domains, so don't blindly accept them.

The allowed domain must include the protocol

You can't just put mydomain.com as a domain, it needs to include the protocol (eg. https://mydomain.com). The fun part is you can't really accept both http and https because...

You can only allow one domain

You can either allow every domain with Access-Control-Allow-Origin: *, or only one. This means that if you need several domains to access your api, you'll need to handle it yourself.

The easiest way to handle this is to maintain a list of allowed domains on your server, and change the content of the header dynamically if the domain is in that list. Here's how it would look in plain PHP for example :

$allowedDomains = [
    "http://www.mydomain.com",
    "https://www.mydomain.com",
    "http://www.myotherdomain.com",
    "http://www.myotherdomain.com",
];

$originDomain = $_SERVER['HTTP_ORIGIN'];

if (in_array($originDomain, $allowedDomains)) {
    header("Access-Control-Allow-Origin: $originDomain");
};

Or in Node.js (adapted from this SO anwser)

app.use(function(req, res, next) {
  const allowedOrigins = [
    "http://www.mydomain.com",
    "https://www.mydomain.com",
    "http://www.myotherdomain.com",
    "http://www.myotherdomain.com",
  ];
  const origin = req.headers.origin;
  if(allowedOrigins.indexOf(origin) > -1){
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  return next();
});

Same Origin Policy applies to the file system on Chrome and Safari, not on Firefox.

If you make a request to a local file, Firefox will consider that it's always on the same domain and allow the request. Webkit based browsers like Chrome or Safari will consider this a security risk and block ajax queries to local files. The only way to get around this is to either use Firefox, or install a web server that will send an Access-Control-Allow-Origin: * header.
As pointed out by @brianjenkins94 in the comments, you could also start Chrome with the --disable-web-security flag.

The iOS WKWebview needs CORS

If you're developing a mobile app that uses a webview (with Cordova or Ionic), Android won't give you any trouble, but the new WKWebview on iOS will require CORS. This means you pretty much have to always set the Access-Control-Allow-Origin header to *, which is really not ideal.
Another alternative is to not make ajax requests in your app and use a cordova plugin to make native http requests, which will happily ignore the Same origin Policy.

Thanks for reading !
If you want a more in depth description of CORS, head to MDN : https://developer.mozilla.org/docs/Web/HTTP/CORS.

Posted on by:

Discussion

pic
Editor guide
 

The only way to get around this is to either use Firefox, or install a web server that will send an Access-Control-Allow-Origin: * header.

You can also run Chrome with --disable-web-security:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir=C:\HighwayToTheDangerChrome
 

Thanks ! I added that tip to the main article.

 

Hi, Brian.

I'm wondering is it okay to disable web security?

 

Not for general browsing, but for circumventing CORS it's fine.

 

Hey man! This article is awesome, I am currently learning CORS, Can I translate your article, learn, and then put it on the China Programming Forum website like Stack Overflow? Website Addr: segmentfault.com/ . And I will attach the original article link.

 

Sure, as long as you include the original link it's not a problem. Thank you !

 

Awesome post Nicolas! I really like your description of how the whole process works. I wanted to share a thought on this piece here you might find interesting:

This is often a source of confusion for newcomers because it's not immediately apparent what CORS is supposed achieve. Firstly CORS is not a security measure in itself, it's actually the opposite: CORS is a way to circumvent the "Same Origin Policy" which is the security measure preventing you from making ajax requests to a different domain.

I agree this is true for the CORS headers, but I think CORS itself means something more general. There is a lot of terminology confusion here community-wide, so I try to go by the spec. Formally any request with an Origin header is a "CORS Request", regardless of what headers are present on the response. The CORS Protocol specification applies to CORS Requests and outlines both when and when not to allow CORS Requests to take place.

AFAIK there is no formal specification for the "same-origin policy", but different specs outline various cross-origin limitations (including those from the CORS Protocol) which is the term the browser error kind of lumps them together under.

Based on that I believe it is technically correct to say that the CORS Protocol is a security measure in itself. However the headers component of the CORS Protocol is definitely a way to circumvent the same-origin policy which I think might have been aligned with what you had in mind for that paragraph.

Thanks again for sharing!

 

Hey, thanks for your input !

You're probably right about that in the sense that SOP isn't a specification while CORS is, and the spec expects browsers to block cross origin requests by default.

However, the introduction to the spec says

User agents commonly apply same-origin restrictions to network requests. These restrictions prevent a client-side Web application running from one origin from obtaining data retrieved from another origin [...]
This specification extends this model in several ways

The way I interpret this is that historically browsers started to implement "Same Origin Policies" before CORS, and CORS was created primarily to allow requests that would never have been possible otherwise.

So while you're technically correct, I think I'll leave my imprecise wording because it personally helped me deal with CORS when I stopped thinking about it as "this thing that prevents me from querying my API" and started thinking of it as "this clever system that allows me to query my API even if it's on a different domain".

 

hi Nicolas,

Great article, you almost saved my life :P

I have one doubt, if the request origin header is is not matching to Allow-Origin-Request-Header value , why it is giving 200 ok as an respose. It shall give 401 if the both side thing is not matching .

Request origin: Evil.com

Response header :
200 ok
Allow-origin-access-control: legitimate .com

why 200 ok, why not 401 ??

 

Because CORS is handled by the browser. The server will never block any requests because of CORS. It's not its responsibility.

The server will always answer with a 200, but add some headers telling the browser "you're only allowed to accept this if the request was done from this domain", and the browser will block the request (and add a lovely warning in the console) if it doesn't match.

Once again, CORS is not meant to prevent malicious user agents from accessing a resource, it's only meant to prevent trusted user agents (browsers) from being tricked into impersonating the user by malicious websites. This means that asking the server to do the check and respond with a 403 would not be more effective, and since you can't expect every backend developer to implement the check correctly you'd still need the browser to check 200 responses and block them if needed.

Hope that helps !

 

Thanks Nicolas for the quick reply, in my case if the response is giving a lot of data than possibility of CSRF is there.

Speaking with server , browser is not only one way to communicate , if i send the request from a server to server with changed origin , if it gives data back to as an response than CSRF can be done. ..Right ??

Correct me if i am wrong .

 

Thanks Nicolas for peeling out all the complexity around CORS and making it super easy to understand.
One can always refer to MDN docs for advanced CORS concepts but your simplified definitions is very helpful for a lot like me.

 

Thanks for sharing!

 

This is a good article!

 

Very clear.
Thanks for sharing

 

Very good article: informative and comprehensive. Thanks!

 

This is great Nicolas, thanks for sharing! I'm definitely passing this to my students who run into this issue often.

 
 

I run into this a lot when dealing with development environments (as a PM), super good read and helped me understand it a bit more!

 
 

typo:

    "http://www.myotherdomain.com",
    "http://www.myotherdomain.com",

you meant to make the second one https in both the php and node example

 

A really good write-up of a much misunderstood problem! Thanks!

 

Excellent article. Extremely clear and well articulated

 

Good, write up Nicolas. Last month, I tried to explain the same with the help of a sample TODO application. You might like it smellycode.com/csrf-in-action/

 

Nice article 👏🏻

 

great info! I liked the conversations between the browser and server 🤓

 

Very Well written article on CORS. Must read for all beginners. Thanks, keep the good work going...

 

Thanks. Nice article...

 

Great article, could you also add the bit about how subdomains work as well, please.

 

Thank you. This was very helpful.

 

This whole post should be invalidated and removed. It's no longer correct and useful after the author decided to use "HIM" for the following sentence:

 Browser thinks to himself "yeah, that's the right domain, I'll send the request !"

Shame

 

I'm pretty sure Firefox identifies as male, not sure about Chrome though 🤔