In this article

If you develop for the web or work on a server, you are bound to run into Cross-Origin Resource Sharing (CORS). Which side of the request you are on will greatly affect your level of frustration, should you get a CORS error in the browser. While relatively simple to implement and maintain, the question of why still lingers for a lot of developers.

The purpose of CORS

The Internet is a wonderful thing, for the most part, allowing for all sorts of information and media consumption as well as productivity. A lot of the value that comes from the Internet is the ability to combine resources from all over. 

Imagine a company that has solved the problem of efficiently and securely storing images. Many other websites can enjoy the benefits of their innovation, allowing for the problem of how to store these images to be solved only once, rather than by every single person who ever wanted to put an image on their website. Going beyond images, websites can share assets such as fonts, style sheets, JavaScript and even raw data.

The problem that arises from this resource sharing is knowing who to trust. Your browser knows how to talk to each individual website and the corresponding cookies and credentials, so it will always try to talk to a website on your behalf as best it can. A bank would have a big problem if another website could simply pretend to be the official one, using the same images and layout, and then simply spy on every request the browser makes to the real bank website for you. This is one of the issues CORS attempts to address. There are still many other avenues that need protection, but CORS can be a simple and straightforward way to establish trust (or lack thereof) between websites.

How CORS works

Because websites are made for humans to view through a browser, the browser is ultimately the tool responsible for implementing the correct behavior according to CORS. This is why many a developer may scratch their head as they make a successful API call in a tool like Postman, but the call fails inside the browser. Ultimately, this is because the browser makes a second request any time it sees that the requested location is different from where the user is currently navigated to called a "preflight."

This "preflight" request is sent to the desired server and essentially asks, "as a browser, should I allow this request from <other location> to proceed?" The server can then decide how to answer that question based on who is asking and what they are asking for. Many sites only care about resources that are user-specific or potentially contain sensitive information. For this reason, it is often possible to pull static resources such as HTML, CSS or JS files, as well as images or fonts that do not play a significant role in the value the site actually delivers.

For a more technical explanation, check out this excellent write-up on web.dev

Implementing CORS

In many cases, if you are using a library to create your server such as ExpressJS, CORS will be provided as a plugin or may even be enabled by default.

const express = require('express');
const cors = require('cors');

const app = express();

const whitelist = [process.env.FRONTEND_SERVER_ORIGIN];

app.use(cors({
    origin(origin, callback) {
        if (whitelist.includes(origin) || !origin) {
            callback(null, true);
        } else {
            callback(new Error('CORS error'));
        }
    }
}));

// attach routes

app.listen(process.env.PORT, () => console.log(`listening on port ${process.env.PORT}`);

In this example, we are using the cors plugin for ExpressJS to create a check that either the origin is that of our own frontend server, or no origin is provided (in the event that the server is being talked to by another server). Creating a whitelist allows for eventual easy adding of other websites or origins we would like to trust, though this is probably better stored in a database to avoid needing to deploy new code any time there is a new trusted origin.

Caveats

In most cases, developers do not need to worry about the more complicated CORS situations. The most common case of CORS is having an API hosted on a separate server from the static website, and as such needs to allow requests from the static location. In the case where the developer does not own the server being requested from, it may be necessary to involve another development team in order to allow cross-origin requests.

Ultimately CORS is not protection for the server, but for the user in the browser. CORS plays no role when servers talk directly to one another and thus is trivially bypassed (unless someone decides to integrate CORS into their requests) so it is important to remember that just because you have implemented CORS correctly on your server does not mean it is secure.

It also does not mean users are safe. If they end up on a fake website that can proxy the requests through another server based on user-entered information, their sensitive data can still be compromised. CORS focuses on protecting from the simpler situations: if someone simply copied and pasted a static site onto a new server, it would not have the same permissions as the original. It also works to protect against malicious code gaining access through legitimate sites such as if a third party JavaScript library is corrupted.

What this means is that CORS usually ends up feeling like a burden to developers — a false positive security flag. However, a browser has no way of knowing what information may be returned in a request or what consequences it would have for the user. When the average person knows very little of what goes on behind the scenes in their browser, it is better to be safe than sorry.