Let’s Talk About Cross-Origin Resource Sharing (CORS)

chuckchoiboi

Chuck Choi

Posted on May 22, 2021

Let’s Talk About Cross-Origin Resource Sharing (CORS)

Access to fetch at ‘https://coolserver.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

Every web developer may have come across this CORS policy violation (Cross-Origin Resource Sharing) error message at least once in their career. I faced this issue for the first time when I was developing a full stack application for a group project at a coding bootcamp. We were simply building a client application that was fetching data from the server we developed, and we panicked as this error popped up.

The error itself is actually pretty informative. It basically tells you that the client-side is not one of the "whitelisted" origins to access the data being fetched. In this blog post, lets learn the basics of Cross-Origin Resource Sharing, three scenarios and the common errors.

What is Cross-Origin Resource Sharing?

Let's first go over what CORS is and why it is important. CORS is an acronym for Cross-Origin Resource Sharing, which is a cyber security mechanism that allows/prevents one origin to access a resource from a different origin. This is something that the server has control over to restrict who has access to the resource, how they can access the data (which HTTP methods are allowed), if cookie information should be included or not, and etc.

Client-side applications are generally very vulnerable to cyber attacks from the malicious users. If you think about it, users can easily open the browser dev tool to check how the DOM is structured, which server it is communicating with, and where the resource is coming from without much restrictions. CORS is not the perfect security measure, but it provides minimum guarantee that the resource we fetch from the other origin is safe.

Same-Origin Policy vs Cross-Origin Resource Sharing

There are two policies that helps the browsers to protect the users from potential cyber attacks via dynamically loaded code. These are Same-Origin Policy (SOP) and Cross-Origin Resource Sharing. Generally, it is forbidden to read data from another origin. SOP allows browsers to only request resources from the same origin. You'd be violating SOP if you request a resource from a different origin. For example, requesting data from https://chuckchoi.me to https://dev.to would be violating SOP normally since these are not the same origin.

This would defeat the purpose and the power of the web if you are not able to fetch data from another origin. Thankfully, Cross-Origin Resource Sharing (CORS) allows exceptions to SOP and permits cross-origin requests to be made. There are three main requests used in cross-origin requests, and let's dive into the common errors you would see for each of them.

Before We Begin...

cors-tutorial

I built a simple client-side React app and an Express server to help us visualize what's going on. There's three different Cross-Origin requests you can test and see common errors you may face based on the server's setting. You can see each scenario's server and request structure, and click "Send Request" button to see what response you would get. You can also open your browser console to check the network tab to see the network behavior. Feel free to use the app on the side to supplement the understanding and check out the repository if you'd like!

CORS Tutorial App Link

Git Repository


Simple Request

There isn't an official terminology for the request we are about to discuss, but MDN's CORS documentation call it Simple Request. Simple Request is a cross origin request that is simply sent without any preflight request (which we will go over next) directly to the server. Server would respond back with a response that contains Access-Control-Allow-Origin in the header which then the browser checks CORS policy violations.

simple-request1

Simple Requests are only allowed if certain conditions are met which is not the case for most of the modern web development. Here are the list of conditions found in MDN:

  • One of the allowed methods:
    • GET
    • HEAD
    • POST
  • Apart from the headers automatically set by the user agent (for example, Connection, User-Agent, or the other headers defined in the Fetch spec as a “forbidden header name”), the only headers which are allowed to be manually set are those which the Fetch spec defines as a “CORS-safelisted request-header”, which are:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (but note the additional requirements below)
  • The only allowed values for the Content-Type header are:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • If the request is made using an XMLHttpRequest object, no event listeners are registered on the object returned by the XMLHttpRequest.upload property used in the request; that is, given an XMLHttpRequest instance xhr, no code has called xhr.upload.addEventListener() to add an event listener to monitor the upload.
  • No ReadableStream object is used in the request.

Wow, that was a pretty long list of requirements. As we discussed, it is pretty rare to meet all the requirements above in modern web development, so you may be dealing with preflight or credentialed request most of the time. But for Simple Request to work without violating CORS error, the response's header needs to have Access-Control-Allow-Origin with the request's origin listed or use an asterisk (* sign) as a wildcard to allow all origins.

Simple Request Exercise -- CORS Tutorial App

  • Error #1: No Access-Control-Allow-Origin Header

Let's go ahead and open up the CORS-Tutorial App. Under the Simple Request tab -> Error 1 tab, this is how the server is structured:

simple-request-error1-server

The fetch method we are invoking is fetch('https://cors-tutorial-server.herokuapp.com/api/simple/no-origin'). By default, fetch() would make a GET request to the URL passed as an argument if the method is not specified. And since the request is very basic, it is sending it as a simple request as it meets the simple request's requirements. Let's go ahead and click the button to see what would response we would get if we make a simple fetch request to that route:

simple-request-error1-response

Based on the error message above, the request we made from the app's origin https://chuckchoiboi.github.io/cors-tutorial/ has been blocked due to the violation of the CORS policy. It shows that "No 'Access-Control-Allow-Origin' header is present on the requested resource."

  • Solution 1: Wildcard Origin

One of the first steps to comply with the CORS policy is adding Access-Control-Allow-Origin to the response's header. You could either specify the origin, or use asterisk as a wildcard to allow all origins. From the server-side, you could add a wildcard origin like this:

simple-request-wildcard-server

Go ahead and try sending the request. You would see the server responding with a valid response like this:

simple-request-wildcard

  • Error #2 - Unmatching Origin

Allowing all origins is probably not the best practice and it would not be secure. It would be better if you "whitelist" the origins by specifying which ones you are expecting. Here's an example of a server with origin specified (Simple Request tab -> Error 2 tab):

simple-request-error2-server

The origin that this route is expecting is https://www.website.notcool though. Making a fetch request from https://chuckchoiboi.github.io/cors-tutorial/ show a bit different error message this time:

simple-request-error2

This time, the error shows that the origin the server is expecting for this route is https://www.website.notcool. Let's say we are making a request from www.website.notcool, but the protocol we are making the request from is http:// and not https://. This will throw the same error since origin consists of protocol and host both.

  • Solution #2: Matching Origin

With that being said, the server's response header would need to have the origin that matches the request's origin. A valid simple request can be sent to a server with the origin specified like this (Simple Request tab -> Valid Condition tab):

simple-request-solution2


Preflight Request

You are going to come across preflight requests more than simple requests in modern web applications. For this scenario, the browser makes a preflight request to ask for permissions before the actual request is made. If the browser approves the response from the server through the preflight request, then the actual request is made. If the preflight request is not approved, then the actual request is not made.

preflight-flowchart

During this preflight process, the preflight request uses OPTIONS method. The preflight response needs to allow the origin of the request in the header, and the actual request's method needs to be allowed as well. Once these conditions are satisfied, that's when the actual request is made.

Preflight Request Exercise -- CORS Tutorial App

  • Error #1: Preflight Response with Unmatching Origin

preflight-request-error1-server

Take a look at this example. The request is trying to make a DELETE request to the server. Since the request is using DELETE method, it will make this request a preflight request, thus the browser will first send a preflight request using OPTIONS method to check its permission. However, since the origin of the request and the response's Access-Control-Allow-Origin value is not matching, this preflight request will fail and not even go to the actual request.

preflight-request-error1

  • Error #2: Preflight Response with Method Unspecified

Let's try again. Let's try sending a DELETE request this time to a route with preflight response that contains header with the request's origin allowed like this:

preflight-request-error2

Does it feel like we may be missing something? Here's a little spoiler. This one again will not even go to the actual request because the server's preflight response does not have DELETE method specified. Here's the error response you will get:

preflight-request-error2-response

  • Error #3: Preflight Passes, Actual Request Fails

preflight-request-error3

Now that the preflight response has matching origin allowed, and DELETE method allowed as well, this will send the actual DELETE request. Did you notice anything wrong from the response header though?

preflight-request-error3-response

You got it right! As the error shows, the server is only allowing https://www.website.notcool origin. Even if the preflight passes, if the actual request fails, you will still be violating CORS policy.

  • Solution

preflight-request-solution

In order to make a valid Preflight Request, the server needs to handle preflight request with valid origin, and valid method in the response header as we discussed. Once the preflight request passes, the actual request is sent. The actual request would need to have the request origin allowed to comply with the CORS policy.

preflight-request-solution-response


Credentialed Request

Last but not least, there's a 3rd scenario to cross-origin request that strengthens the security. When sending XMLHttpRequest or fetch, you should not include the browser cookie or authentication-related headers without any options. Sending a request with credentials option would allow us to send sensitive information like cookies in cross-origin requests.

You can send a credentialed request by adding {"credentials": "include"} option to the request in JavaScript. This will add some strict rules to CORS policy conditions. When the browser is sending a credentialed request, the response's Access-Control-Allow-Origin should not be using the wildcard "*". It needs to specify the request's origin, and also the server needs to have additional header Access-Control-Allow-Credentials set to true to allow valid credentialed request to be made.

Credentialed Request Exercise -- CORS Tutorial App

  • Error 1: Wildcard Origin

credentialed-request-server

This time, we are sending a GET request using fetch() method that includes {"credentials":"include"} as an option. The server's response header is using a wildcard for Access-Control-Allow-Origin. Let's go ahead and send the request by clicking the button in the app.

credentialed-request-error1

As you can see from the error message, credentialed request does not allow Access-Control-Allow-Origin to be the wildcard. In order for us to be able to make a credentialed request to the server, we will need the server's route to allow https://chuckchoiboi.github.io.

  • Error 2: Access-Control-Allow-Credentialed

credentialed-request-error2-server

Okay we have the request origin https://chuckchoiboi.github.io specified in the server this time. Without further ado, let's go ahead and click the "Send Request" button!

credentialed-request-error2-response

Trust me, this is the last error you are going to see today. As we discussed earlier, credentialed request adds more strict conditions to CORS policy rules. What the error message is suggesting is that the response header needs to include an additional header Access-Control-Allow-Credentials with its value being set to true.

  • Solution

To summarize, credentialed request can be made by adding {"credentials":"include"} in the request, response header specifying the request origin (wildcard not allowed), and Access-Control-Allow-Credentials is set to true in the response header as well. A successful credentialed request should look something like this:

credentialed-request-solution


Conclusion

When I think of Cross-Origin Resource Sharing, it reminds me of guest list/access to a gated community. I've been to couple of my friends' gated community houses, where the home owners have to give names to the security guard at the gate to let them know who is invited to enter the gate.

gate-security

What's interesting about Cross-Origin Resource Sharing is that the front-end developers are the ones who actually have problems with CORS policy violations, while the backend developers have the control to resolve these issues in the response header. Resolving CORS errors is not too difficult to handle, you just need to communicate with the backend developer to make sure all the CORS policy conditions are met to fetch the resource.

Thank you so much for taking time to read this blog post! If you want to learn about the React app or the Express server I built, or give me feedback on the app/blog post feel free to message me on LinkedIn!

💖 💪 🙅 🚩
chuckchoiboi
Chuck Choi

Posted on May 22, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related