Web Security 101: Understanding Cross-Origin Resource Sharing (CORS)

Web Security 101: Understanding Cross-Origin Resource Sharing (CORS)

Continuing the web security headers series, after covering HSTS (Strict Transport Security), and CSP (Content Security Policy), now comes a more painful security header, so to speak. Painful, at least for web developers.

And if we don’t want to generalize, CORS represented a painful header, or something that I always needed to bypass on the localhost environment when started working on a new app.

But what is CORS?

Cross-Origin Resource Sharing (CORS) is a system that allows servers to indicate origins other than their own. It uses HTTP headers to determine whether browsers blocks frontend JavaScript code from accessing responses for cross-origin requests.

Why?

The browser is quite strict (in a positive sense), which means that we need to specify what origin should be allowed in order to let JavaScript code access certain services that are not hosted by our own origin (domain, scheme, or port).

Example?

If we have our frontend app (React, Angular, Svelte, Vue.Js, or without frameworks), up and running on port 3000, and the backend running on port 5000 (again .Net, Java SpringBoot, Python’s Django, JavaScript’s node.js, etc), we will see how browser blocks cross-origin requests by default:

Consistently, every new project I worked on ran into these CORS issues, which I then resolved by setting up a proxy for the local environment.

Developers commonly use Express or custom code with Node.js to configure a proxy as a CORS middleware. And the CORS fix would look like:

While it’s something that fixes the problem, looking back in time, I became less of a fan of * (allowing all) as settings.

Even though on localhost it’s not really a problem. But exercising correct headers can be helpful, so instead we can manually set the required CORS headers:

CORS in action

I would recommend keeping the “*” solution only when time is limited. This is to avoid forgetting the “allow all” setting and deploying the solution to the production environment without enforcing a more strict security header.

Risking not only security vulnerabilities (Cross-Site Request forgery attacks, and exposing sensitive data to unauthorized domains), but it can pose even business risk, because it enables free access to scrape API’s, and it limits control of who consumes your resources.

That’s why the most effective solution is not only to allow certain origins, but to express what methods and headers can the origin execute:

So right now because I want to be able to make a request from journeypixel.com, I will set the origin there, and after the methods and headers, we have the option of a max-age (seconds).

max-age?

Access-Control-Max-Age specifies the number of seconds for which the response to the preflight request can be cached without sending another preflight request. Bear with me.

This needs to be kept in mind, for security reasons, we should not set the max age setting for too long.

The default value is 5 seconds. In the present case, the max age is 86400 seconds (= 24 hours). It’s important to note that each browser has its own internal limit, which overrides the Access-Control-Max-Age value if it’s larger.

So, Preflight?

Preflight is a browser mechanism, a preliminary check that browsers automatically make before certain HTTP requests. This is one of the cases.

Like asking for permission first: I will plan a trip to Spain, so instead of going all the way, and landing in Spain just to find out I am denied entering there, the preflight is essentially that, a notification sent to Spain’s airport so in return I would receive a go or no go.

So this means when we want to request (a POST for localhost:5000/api/data), we will see two network requests instead:

Then, the next step would be in the same OPTION preflight request to see a (happy flow) positive response:

After, the second request is the one we initially sent:

Back to HTTP headers

As in general when discussing browsers and its relationship with headers, sometimes (and rarely we know why) the browsers handle their own caching mechanism. So even if we set specific dates, there are times when either due to updates, or various other reasons, the browser decides to cache more than expected.

Why does the browser caches so much?

We will discuss in a separate topic. But in short, browsers want to be fast, or to appear that they load the web app instantly.

Because leaving security aside, no one wants to use a website that loads in minutes instead of seconds. But wait, some people do use the TOR browser. This is the only joke in the article, I promise.

Back to CORS

So what does CORS protect against?

CORS prevents a specific kind of attack that happens in the browser, one that we mentioned earlier called Cross-Site Request forgery. In essence, CORS is designed to stop websites from making unauthorized requests using your browser’s credentials.

CSRF attack

As the over-used phrase go “security is the strongest as it’s weakest link“, we can clearly see how this header would prevent this kind of attack, that revolves around social engineering and phishing attacks, some of the more prevalent ones that occur lately.

Keep in mind

It doesn’t prevent people querying your backend from curl or Postman because cause that’s not how that type of attack works. It basically protects your end user from having their credentials spoofed and your backend from falling for it.

In essence, CORS is a browser-only security measure, and one of the most important security headers out there. I know, I say about all of them the same thing. And they all are important.

Photo by Christopher Gower on Unsplash.