CORS. What it is, how it works, what it's for, and how to solve it

If you design web applications that consume APIs, I'm sure you've encountered the problem of Cross Origin Resource Sharing (CORS) at some point. In this document, we will see why it happens, what it's based on, and how it can be solved.
CORS. What it is, how it works, what it's for, and how to solve it

When we talk about CORS (Cross-Origin Resource Sharing), we refer to a security mechanism that browsers apply when making a request to a resource that is hosted on a different origin. If the resource is on a different origin, the browser will automatically check the HTTP headers for explicit authorization from the server.

To review, I remind you that the concept of origin is the combination of protocol, domain, and port. That is, CORS applies when the origin is different, so it checks that it is the same protocol (http or https), the same domain (excluding subdomains), and the same port (80, 443, 8080, etc.). If the origin making the request does not match the origin of the resource, CORS comes into play.

For example, imagine that you are designing an application located at myapp.es and the data requests are made to an API located at api.myapp.es. As soon as you make a query using the JS fetch method to the domain (or subdomain in this case where you have the API), you will encounter the following error:

CORS ERROR: CORS error example

This not only happens when the call is made by JavaScript. You will also see CORS errors when trying to load web fonts (using @font-face), videos, textures, etc

Next, we will see what CORS is exactly, what it can protect you from, and how to remove this error. And one important thing: CORS errors are at the browser level, so you cannot debug using JS, so the only debugging mechanism is through Dev Tools 🤷‍♂️.

What does Cross-origin resource sharing protect against?

In short, CORS protects against malicious sites interacting with legitimate sites. Let me give you an example if CORS did not exist: imagine you are browsing the internet, and you enter a legitimate site like Facebook. For some reason, while you're browsing Facebook (where you already have a session initiated), you click on a post that takes you to a malicious site like BadSite.com. The following will happen:

  • BadSite.com downloads and runs malicious javascript code in your browser that sends a request to Facebook (such as a request that forces Facebook to return your token, your credentials, your email, etc.)

  • Facebook receives the request from BadSite.com and without CORS, no one verifies if that origin is legitimate and can make requests. So Facebook returns your data to the script of BadSite.com (because the only way to check that it's you is that you've logged in, and you did it through the browser).

  • BadSite.com receives your data, and here is where the problem begins: spam lists, bank fraud, and other security issues of today.

Thanks to CORS, in point two of the list, Facebook would return a CORS header that only authorizes requests to be sent to certain authorized websites and never to SitioMalo.com. That is why your browser automatically blocks the response, and SitioMalo.com will never be able to read the data that Facebook has returned.

Therefore, you have to see the CORS and same-site policy as your ally in security, never as something that hinders it. The security of users depends, in part, on CORS (and other policies) working and on developers following them scrupulously.

Okay! At this point, I think you're ready to understand what CORS is all about.

What is CORS? Simple Requests and Preflighted Requests

There are two types of CORS, or rather, two ways of acting based on the type of request that is made. Let's start with Simple Requests

Simple Requests. The friendliest CORS

In this type of request, the browser makes the request and waits for the response. When it has arrived, it analyzes the headers that the web server has sent us and looks for the CORS header Access-Control-Allow-Origin. It will not look for any other CORS headers.

Basically, this header indicates which origins (which web addresses) can read the response that has just been sent. If our website is at "myapp.com" and there is the header "Access-Control-Allow-Origin: myapp.com", great! You can display the content. Otherwise, the browser discards the response and returns an error to Javascript.

For the CORS protocol of a simple request to be applied, the following requirements must be met:

  • It must be a call made with the HEAD, GET or POST methods.

  • Apart from the headers that browsers automatically add (and over which we have no control, such as Origin, Connection, or User-Agent), the request can only carry at most the following headers (or some or none; but in no case any other): Accept, Accept-language, Content-Language, Content-type.

  • The Content-Type is limited to application/x-www-form-urlencoded, multipart/form-data, and text/plain.

If the request we have made meets these requirements, the browser will only look for the Access-Control-Allow-Origin header.

Verified Requests. What is preflight?

It is possible that the request we are making does not meet the aforementioned conditions, for example:

  • We include an invalid Content-type, such as application/json.

  • We attach cookies or credentials of some kind (very common in apps).

  • We use a method like PATCH, PUT or DELETE.

  • Several of the above at the same time.

In this case, the browser considers it necessary to add an extra layer of security and the less friendly CORS comes into play: the preflight. This means that before making the actual request, the browser will send an alternative OPTIONS request to the web server to evaluate if it is a verified call. It's like the browser, before making the actual request, calls the target server to make sure it can do it safely. And no, the developer can't do anything here: it's a transparent process that the browser does for us.

In the preflight request, the browser will send the following headers to the server (as if giving it information about what is going to happen and if it allows it):

  • Origin header: indicates the current origin; the website we are on.

  • Access-Control-Request-Method header: what HTTP method we are going to use.

  • Access-Control-Request-Headers: what headers we are going to attach in the request in addition to the automatic and basic headers. Examples are: authorization, x-whatever, etc.

In this case, the wildcard * cannot be used. It must be specified specifically.

The web server receives this OPTIONS request and responds to the browser with the following headers:

  • Access-Control-Allow-Origin. It tells us which origins are allowed. If it matches the current one, great; if not, it aborts the connection.

  • Access-Control-Allow-Methods: list of allowed HTTP methods, such as POST, PUT, GET... If the method matches, we can proceed!

  • Access-Control-Allow-Headers: list of allowed headers. If the headers you were going to add to the request are here, and the rest of the headers don't say otherwise, you can make the actual request!

Generally, the server determines whether to allow the call based on the information sent by the browser. Therefore, the server first needs the client to contact it. With that client information, the server can dynamically reject the connection by changing the headers to impossible values.

Response CORS Headers

CORS is a security protocol that web browsers perform when a web request has a URL with a different origin than the current one. If this is the case, the browser will look for the following headers in the web server's response and stop the call if something is not correct.

Access-Control-Allow-Origin

The browser looks for this header in the response and checks if the information contained in that header matches the current origin (the website you are visiting). If there is a match, there is no problem; but if not, the call will be stopped and not completed. This means that it's as if it hadn't been made.

That is, if the header has the value "Access-Control-Allow-Origin: app.es", the response can only be read and processed if we are on that website. If someone makes a call from another website, we won't be able to read the response.

This header allows the wildcard value "*", which means that the response is readable from all origins. An API (which can be called from countless origins) will use * to avoid limiting where it can be called from.

Access-Control-Allow-Origin: <origin> | *

Access-Control-Allow-Methods

This response header indicates which methods are allowed. For example, we can indicate that only GET and POST requests are allowed to a specific resource (but not PUT or PATCH). The browser checks if the HTTP method we use to make the request is one of those listed here. Again, if the method is not on the list, the request will be blocked.

Access-Control-Allow-Methods: <method>[, <method>]*

It is only used in verified responses.

Access-Control-Allow-Headers

This response header indicates which headers can be included in the originally made request. If the headers that we have attached to the request are not listed in Access-Control-Allow-Headers, the call will be cancelled and cannot be processed.

We should note that in addition to the headers that browsers automatically add, the following headers are always allowed (there is no need to add them to Access-Control-Allow-Headers):

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type

That is, if we don't declare any Access-Control-Allow-Headers and we add, due to a necessity of our platform, the header X-USER: 1, the connection will be interrupted. We must add the response header 'Access-Control-Allow-Headers: x-user' on the web server.

Access-Control-Allow-Headers: <field-name>[, <field-name>]*

It is only used in verified responses.

Solutions

Unfortunately, in production, there is only one valid solution: configure your web server (Apache, Nginx, etc.) to serve resources with the correct same-site policy. So, you will have to fight with it.

But if you are currently doing some testing locally or with a small group of users (who you can ask to do some tricks), you can apply some "temporary" adjustments or solutions so that CORS does not bother you during the programming and testing phase (but in the end, you will have to correct it).

The first option is to add the Access-Control-Allow-Origin: * header. As we saw earlier, this means that any origin can display the response. It is quite insecure, but during development, you can afford it.

The second, simpler option if you don't want to complicate your life, is to download an extension for your browser that handles bypassing or disabling CORS. It depends on the browser you have and the guarantee or security the extension gives you, but in general, any extension will do. These extensions take care of adding or modifying the necessary CORS headers when a request arrives to bypass this security policy.

The third option is to use a proxy. This proxy is placed between the browser and the final destination (the URL to which you want to make the request). It takes care of making the original request to the destination and returning it to the browser (either with modified CORS headers or without CORS because it is in the same domain). If, for example, you are using React, you can create a proxy in the same domain following this guide.

Conclusion

We've all been there when we're developing a website or an application, and CORS has caused some trouble. And when you don't understand very well why or what for, that little problem can become a setback.

But you must understand that CORS is there as a security policy to safeguard user information and to try to make the Internet a little safer 🔐.

Configuring a web server such as Apache or Nginx to deliver resources with the correct headers can be a bit complicated the first time, but once you go through that process, you'll see that it's easier the next time.

I hope you've learned a little more about CORS, how it works, and how you can make it not cause you too many problems.

And you can also learn how to configure an Nginx server to support CORS, even on multiple origins

Happy coding!