CORS : Understanding Cross Origin Resource Sharing

Introduction

If you have ever worked on some ajax calls or a react SPA you might be familiar with the annoying CORS error. The go to solution for us in that case is to talk to the backend guys and ask them to just allow everything because YOLO. But what is CORS? Is it just a way to annoy frontend developers? Has it got anything to do with security? If yes then why do we need auth and secret keys? If no then what purpose does it solves? How does it work in first place?

If you have these questions, this article is for you. It will try to answer all of these and also propose solution for the issues you might face while dealing with CORS.

What is CORS?

To understand what CORS (Cross-Origin Resource Sharing) is, first we need to understand what Same Origin Policy (SOP) is. SOP is a security measure implemented by all modern browser which disallows scripts and resources loaded from one origin to interact with another origin. In other words if your website is hosted using www.example.com, then you can't make XHR requests to www.test.com. How does this helps? Consider a scenario where you are already logged in to facebook and you open a malicious website. This website can make requests to facebook and extract personal info from your logged in session. To prevent this, SOP is implemented in browsers. SOP doesn't restrict access to servers, we use mechanisms like api keys and secret key for that. In fact server is unaware of this whole thing and you can make the same request using curl or postman and things will work.

If SOP is a way to restrict cross origin access, CORS is a way to circumvent that and allow your frontend to make legit requests to a server. If your client is hosted on a different origin then your server, your client wont be able to make xhr requests to your server due to SOP. CORS allows us to do that in a secured and managed manner.

According to the MDN Web Docs:

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin.

What is an origin?

We have already been using the term origin loosely and will continue to do so for the rest of the article. So its good to know the intricacies involved around an origin. An origin is made of three parts : scheme (protocol), host (domain), and port. Two origins are said to be same if all three of these components are equal. They are different if any of one these component is different. For example http://example.com/app1/index.html and http://example.com/app2/index.html are same origins, so are http://example.com:80 and http://example.com (default port for http is 80). But http://example.com/app1 and https://example.com/app2 are different origins as protocol is different. Similarly http://example.com and http://www.example.com are different origin as domain is different.

How does CORS work?

CORS specification allows server to send back some headers in response which client understands and based on these headers browser can make a decision if they want to serve the request or not. There are multiple such headers but the main one is Access-Control-Allow-Origin. The value of this header can be * which means the server has configured to allow everyone to access the resources. Or it can be the specific origin it has allowed :

Access-Control-Allow-Origin: https://example.com

There are two types of CORS request: "simple" requests, and "preflight" requests, and it's the browser that determines which is used. As a developer you don't need to make this distinction however its good know how these two types function for debugging purpose.

Simple Requests :

An API request is deemed as simple request if it meets all of the following criteria :

  • API method is one of these: GET, POST, or HEAD.
  • Content-Type header has one these values : application/x-www-form-urlencoded, multipart/form-data, text/plain

These two will make up most of the simple request use cases, however a more detailed list can be found here.

Now if your API requests is deemed as simple the browser will go ahead and make the call to server, the server will respond with CORS headers. Browser will check the for Access-Control-Allow-Origin header in the response and proceed accordingly.

Pre Flight Requests :

If your API call doesn't satisfy the criteria of being a simple request (most common is Content-Type value being application/json) the browser will make a request before sending the actual request. This request which is being made before making the actual request is called preflighted request. Pre-flight request will contain all the information of the actual request and will be made with method OPTIONS. Server will reply to this preflight with all the CORS headers for actual API call and thus browser will know that it can go ahead and make the actual API call or not.

Lets take an example, we are trying to make a GET call to https://example.com/status. The Content-Type is application/json and thus the browser doesn't qualify it as a simple request. Hence browser will make a pre flight request before making this request :

curl --location --request OPTIONS 'http://example.com/status' \
--header 'Access-Control-Request-Method: GET' \
--header 'Access-Control-Request-Headers: Content-Type, Accept' \
--header 'Origin: http://test.com'

This is browser telling server that I am going to make a call with GET method, Content-Type and Accept as headers and from origin https://test.com. The server will respond to this request as :

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: OPTIONS, GET, HEAD, POST
Access-Control-Allow-Headers: Content-Type, Accept
  • Access-Control-Allow-Origin: The origin that is allowed to make the request, or * if a request can be made from any origin.
  • Access-Control-Allow-Methods: A comma-separated list of HTTP methods that are allowed.
  • Access-Control-Allow-Headers: A comma-separated list of the custom headers that are allowed to be sent.

Browser will interpret this and check if our status call can be made. In our example the server responded with * for origin, thus now browser will make https://example.com/status call. If the origin would from pre flight response would have come something like Access-Control-Allow-Origin: http://domain.com, we would have encountered the Cross-Origin Request Blocked error.

Dealing with CORS errors

We now know what CORS is and how it works. One thing to note from above is that complete control over CORS lies at the server i.e. server can allow and disallow origins. So how can we fix issue which pop up when we don't have access to server code? Lets look at these scenarios one by one.

CORS when developing servers

If you are building a server and want to serve some of the clients, just make sure that you allow all the origins your clients are going to make the requests. You can send only one origin in response but you can maintain a whitelist of origins on your server and send the back the requested origin in header. Here is a way to do this in node :

app.use(function(req, res, next) {
  const allowedOrigins = ['http://127.0.0.1:8020', 'http://localhost:8020', 'http://127.0.0.1:9000', 'http://localhost:9000'];
  const origin = req.headers.origin;
  if(allowedOrigins.indexOf(origin) > -1){
       res.setHeader('Access-Control-Allow-Origin', origin);
  }
  res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', true);
  return next();
});

If you like to live on the edge you can go ahead and allow all the origins to make requests to your server using Access-Control-Allow-Origin: *.

CORS when developing clients

This is the case where you don't have any control over servers, i.e. you are making a third party call and cant contact the developers to add your origin. This is a tricky situation to be in as you wont be able to make any XHR requests to this third party. In this case you might want to change the server itself i.e. you create a proxy server which allows your origin and then that proxy server will make the third party call. As mentioned before servers don't understand SOP and hence the proxy server can call the third party server without any issues just like any other client like postman. All you need to do is create a proxy server and send correct CORS headers from this proxy server. There are ready made solutions for this use case like core-anywhere.

Conclusion

We learned about what is SOP and how CORS is used to circumvent the restrictions imposed by SOPs. We also looked at different ways an API is dealt with for CORS by browser and different headers which comes with it. Finally we looked at scenarios and solutions to deal with Cross-Origin Request Blocked error when developing applications.