A seasoned backend developer sheds light on misconceptions about Cross-Origin Resource Sharing (CORS), a technology often suggested as a solution to fix the backend.
Recent work I did on fastify-cors, the official CORS plugin for the hugely scalable fastify web server, prompted flashbacks of many previous backend projects requiring cross-origin resource sharing. CORS has remained a technology shrouded in mystery. So I decided it was time to dive in and attempt to understand why CORS exists and why it brought back so many conflicted feelings.
A typical day as a backend developer
Having spent most of my career building scalable backend web services, I’ve experienced the same perplexing request on nearly every project: The frontend is broken — please add CORS to the backend.
On many such projects, a quick web search for the correct npm package or configuration option revealed how to “enable” CORS, prompting the question: What does “enabling” CORS actually do? All the services I’ve built simply allowed all requests to work with CORS. Was CORS even enabled? It always felt like I was forced to add a Fastify plugin, an express or koa middleware, or set an option in hapi to fix the frontend. Once that was done, the frontend guys were happy and the development continued on as usual.
CORS always remained a mystery — and more of a hassle than anything else. Every time I attempted to understand it, I ended up wanting to know less: origins, preflight request, allowed headers, etc. I did, however, uncover a few tricks to help solve the mystery, which I’ll share here.
Secret 1: It’s a browser problem, not a server problem
Key to understanding CORS is realising that the CORS protocol is enforced by the browser. All those frontend devs who said my backend was broken were wrong.
Now, that is a huge simplification. In fact, the solution always requires a change on the server. However, before talking about the solution, we should go way back through the tunnel of time to unravel the CORS mystery.
Back then, pages were simple and browsers could be strict. Pages could embed images, CSS, scripts and a short list of elements such as forms from any other web server. Put in modern CORS vocabulary, any page could embed any of these elements from any origin. However, when a page issued an XMLHttpRequest, browsers would only allow those requests to be made to the same server that hosted the script making the request.
This is the origin. It was an attempt to limit the security threat from malicious pages turning browsers into malicious actors by executing malicious scripts. It was a scary time, with a simple and restrictive solution.
Client-side technologies such as Ajax programming, built upon XMLHttpRequests, were gaining popularity. A clear need existed to securely allow some “cross-origin” requests. The idea of CORS was born (although the CORS standard was accepted much later, in 2014).
Fast-forward to the present time, when single-page applications are standard, and sites can be rendered on the client, server or a mix of the two. Today, XMLHttpRequest requests form a foundational building block to allow for the modern web. The modern browser fetch API and the once popular Ajax programming paradigm are technologies built on XMLHttpRequests.
Secret 2: CORS is enforced in the browser
CORS is a security mechanism implemented in modern web browsers. According to the mechanism, if a script hosted on server A tries to make a request to server B (a cross-origin request), the browser will respect the CORS protocol. The browser will ask server B if they would allow the request from server A. If server B does not participate in CORS or replies by saying it will not allow the request, the browser reports the original request as failing. The enforcement of the protocol is done by the browser (client).
Secret 3: Servers must opt in
If server A wants to allow access to scripts hosted on server B, server A must comply with the CORS protocol. If the server does not comply with the protocol, the old rules apply: Most importantly, no XMLHttpRequests requests will be allowed.
Viewed in this way, servers must opt into CORS and allow browsers to execute requests made by scripts served by other origins. This is why the CORS burden always falls on the backend developers.
The reason backend developers are asked to add CORS to the server is because the frontend is often hosted on a different server. The scripts running on a web app and those on the backend have different origins. This is especially true when developing an application.
Typically, the frontend and backend are debugged separately. In many organizations, this can even involve separate teams. Even when taking a full-stack approach to development, the front and backend may be running on separate ports on the development machine. Most browsers will treat requests to a different port as cross-origin and require the backend server to support CORS.
Secret 4: Not all requests are created equally
When talking about CORS, browsers put requests made by scripts running on a page into two categories: simple and preflighted.
The browser will make simple cross-origin requests to the server. However, the browser will not share the response with the running script if the server does not participate in CORS (by adding some required headers to the response). Instead, the browser will report the request as failed. In this case, it is important to emphasise that the actual request is still made to the server. A server that is not aware of CORS will just assume that the request was like any other.
The browser pauses preflighted requests temporarily. This allows the browser to ask the server if its CORS policy allows the paused request. The server does this by making an HTTP OPTIONS request. The HTTP OPTIONS method rarely has a place in REST APIs, but this is not the case with CORS. The browser makes the OPTIONS request with some extra headers to ask if the paused request is allowed. If the server replies with the correct response with CORS headers, the browser will allow the paused request to be made. However, if the server does not reply to the OPTIONS request and does not adhere to the CORS protocol, the browser will not make the original request and report it as failed. The special OPTIONS request that happens before the complex request is called a preflight request.
What makes a request simple or preflighted? Those rules are defined by the CORS specification. The basic idea is that requests that wouldn’t modify something on the server (such as GET requests) are considered simple requests. These requests are made to the server, and CORS is enforced on the response. It’s not just the HTTP method that is important: Certain headers are also taken into account when categorising simple and complex requests.
Secret 5: CORS does not replace authentication and authorisation
As noted in secret 2, CORS is enforced by the browser. CORS does not replace server-side authentication and authorisation. It’s easy enough to write a server-side script to bypass the browser and any security added by CORS. What’s hard is to deploy a malicious webpage that will execute malicious scripts in a modern browser and bypass CORS. This is because CORS is a solution to a browser problem described in secret 1.
CORS was designed to avoid impacting existing server applications, and it can be configured at the IIS or Apache web-server level. This works by adding a layer that manages the CORS headers and responds to OPTIONS requests on behalf of the server application.
Considering the above points, it is understandable that CORS does not replace server-side security best practices. The logic behind a particular server-side API endpoint does not even need to be aware that the server implements CORS. It is crucial to keep your server secure and always implement proper authentication and authorisation.
Enabling better applications
Learning these secrets has helped me remove the mystery surrounding CORS. Next time a frontend developer says the backend is broken, I will smile and say I can fix their browser problem. A great tool for building backends that scale to an astonishing level is Fastify. Be sure to check out fastify-cors to add the CORS magic to your Fastify server.
Cody Zuschlag is a Senior Software Engineer at Nearform and a part-time instructor at the Université Savoie Mont Blanc in Annecy, France. He has extensive experience working with node.js, cloud-first applications, and managed databases. His passion is creating the best developer experience and sharing technical knowledge to enable developers to create the best solutions. You can connect with Cody on LinkedIn.