Understanding CORS: A Practical Guide
Have you ever seen a message in your console like: "Access to fetch at '...' from origin '...' has been blocked by CORS policy"? CORS doesn't draw attention to itself when everything is working, but at a crucial moment, it firmly blocks unauthorized actions. For example, reading the response to a cross-origin request without the server's permission.
Web technologies allow us to perform banking transactions instantly, make payments in online stores, collect and process data - but the more actively websites communicate with each other, the more pressing the issue of security becomes.
CORS is a protection mechanism for cross-origin requests, but the first time I encountered this error, I didn't understand how to fix it. I tried adding the required header mentioned in the error message (we'll look at where to find these errors later), but that didn't help. I had to dig deeper: to understand what an origin is, how a simple request differs from a preflight request, why using a wildcard (*) doesn't work with credentials, and where CORS ends and CSRF begins (don't worry too much about these terms now, they will be explained throughout the article).
In this article, I will briefly answer questions about why the CORS policy was created, how it works, why a simple action like "setting a header on the backend" might not be enough, and what secure patterns to choose for the frontend.
To understand the logic behind CORS, we need to figure out where this security policy started - namely, with the Same-Origin Policy (SOP): what it allows, what it forbids, and why CORS wouldn't be needed without it.
What is SOP and Origin
It all started back in 1995 when everyone's beloved (or not so beloved) JavaScript appeared and its use was implemented on web pages. At that moment, the concept of a browser policy emerged, and it was called the Same-Origin Policy (SOP).
SOP is a fundamental principle of browser security, guaranteeing that scripts from one Origin cannot access data from another origin without explicit permission. Initially, SOP only protected access to the DOM (page structure) of other origins, but it was later extended to other sensitive objects (like cookies and global JS objects).
So, what is an Origin? An Origin (hereinafter referred to as Source) is a unique combination of scheme (protocol), domain, and port (see the diagram below). If at least one of these components differs, one Source will be different from another.
Examples of Matching or Non-Matching Origins
The comparison table below provides examples:
How SOP Works in Practice
Let's now break down a scenario step-by-step where the SOP policy might be triggered (Image above):
What Would Happen Without SOP
Why was SOP necessary in the first place? What's the benefit for me as a user? And if I'm a web developer, why should I keep this in mind and work around the restrictions?
It's hard to argue with the primary reason: security.
Let's imagine SOP doesn't exist and play out a scenario (image above), disastrous for both the user (who becomes vulnerable) and the developer (whose product loses trust):
Without SOP, this script could get the user's recent transaction list, create a new transaction, etc. This is because, according to the original concept of the World Wide Web, browsers are obliged to add authentication data like session cookies and authorization headers at the platform level when making requests to the bank's site, based on that site's domain.
Let's return to our reality, where a protection mechanism against such nuisances exists (image below). Steps 1-3 from the disastrous scenario would be the same, but at step 4, SOP would block access to the requested resources.
It was precisely to prevent such attacks that the Same-Origin Policy was introduced: browsers started automatically blocking scripts from one Origin from accessing data from another.
Important: Although JavaScript indeed doesn't have direct access to the bank session cookies, it can still send requests to the bank's website using those bank session cookies, as in the situations on images above.
Experienced readers might say you can simply set the HttpOnly flag on cookies. However, this flag only became a standard in 2002. Others might mention SameSite. But it only appeared in 2016 and became a standard only in 2019-2020.
SOP restricts reading data from a foreign origin, but it does not block the sending of requests to foreign domains. The browser would still automatically include cookies for bank.com when submitting a form to bank.com - it's just that the script from bad-site.com wouldn't find out what the bank returned. SOP only prevents the attacker from reading the response and confirming the attack worked. To protect against such scenarios on the server side, additional measures are needed (e.g., CSRF tokens in forms, setting appropriate SameSite and HttpOnly values, etc.).
This is the Same-Origin Policy (SOP) in the browser, designed to protect the user. It doesn't seem too complicated overall, right? The user doesn't need to think about it because the browser developers have already thought about it and implemented a protection mechanism. The developer also doesn't need to worry about it specifically in their code - it's enough to follow the established rules.
Restrictions Imposed by SOP
So, SOP imposes a number of strict restrictions on interaction between resources from different Origins:
Essentially, SOP says: "you can't read others' data," and this provides basic isolation. But the real web has long been multi-domain: we pull fonts from CDNs, call external APIs, facilitate communication between microservices. How to make such exchanges legitimate and secure without breaking isolation? This is where CORS comes onto the stage - a set of rules for coordinated access between different origins.
The Emergence and Role of CORS
SOP remains the foundation of browser security. But to allow controlled crossing of boundaries between origins, Cross-Origin Resource Sharing (CORS) was standardized: it adds explicit rules and headers to SOP, allowing the browser, based on server responses, to precisely grant access to clients from other Origins.
In essence, CORS is a browser technology that grants web pages access to resources from another domain under certain conditions.
How the CORS Policy Works
Let's say we have a frontend application on one domain (https://www.a.com) that wants to request data from an API on another domain (https://www.b.com). By default, SOP forbids the script from reading the response. However, the CORS standard defines a number of HTTP headers that the server (domain B) can use to tell the browser: "I trust domain A, you can let it read the response." This happens through embedded headers that the browser uses to regulate access between Origins.
Now let's look at how CORS works in more detail and see which headers the browser relies on. When the browser makes an AJAX request (Fetch or XHR) to a third-party resource, it automatically adds an Origin header to the request, indicating the current origin of the page. For example, a request from the page http://www.a.com/page.html to the resource http://www.b.com/data.json would look like this (image below, step 2):
Recommended by LinkedIn
GET /data.json HTTP/1.1 Host: www.b.com Origin: http://www.a.com
The server www.b.com, having received such a request, can decide to allow access. To do this, it must include the Access-Control-Allow-Origin header in the response, with a value of either the specific requesting domain-origin or * (the asterisk means "allow for any origin"). For example:
Access-Control-Allow-Origin: http://www.a.com
If the browser sees Access-Control-Allow-Origin in the response with the required origin (or *), it will not block the script's access to the received data. Otherwise if such a header is missing - blocking will occur: the JS code will get a network error instead of the data.
Besides the main permitting header, the CORS standard defines other access control headers:
But I want to note that requests can be different when viewed through the lens of CORS. This brings us to concepts like "simple" requests (see the image above) and "complex" ones (see the image below) (requiring a preliminary check via a preflight request).
Simple and Complex Requests in CORS
A simple CORS request is one that does not require an additional "handshake" with the server. The browser sends it immediately, only adding the Origin header, and expects a direct response with Access-Control-Allow-Origin.
So how is a request determined to be complex? What rules does the browser rely on to determine the type of request? The standard defines characteristics for simple and complex requests.
If all the requirements for a simple request are met, it is considered simple and the browser will send it directly. However, if just one condition is violated for example, specifying an Authorization header for a token, or using the PUT method the browser will, before the main request, execute a special preliminary request (preflight) with the OPTIONS method to the same URL. This OPTIONS request does not contain a body but includes the headers Access-Control-Request-Method (with the method of the main request) and Access-Control-Request-Headers (a list of non-standard headers, if any).
Thus, the browser asks the server if it allows a request with such parameters. The server must respond to the preflight request with a status of 200 (or 204) without a body, but with the previously mentioned headers: Access-Control-Allow-Methods (listing allowed methods, e.g., PUT), Access-Control-Allow-Headers (listing allowed non-standard headers, e.g., Authorization, X-Custom-Header), and the mandatory Access-Control-Allow-Origin (specifying the origin or *).
If the browser receives a favourable response, it will proceed and execute the real request (e.g., PUT with the specified headers). And in response to the real request, the server must again include Access-Control-Allow-Origin (and, if needed, Access-Control-Allow-Credentials) so that the browser delivers the data to the script.
Important note: This entire exchange happens automatically, without intervention from the front-end developer but if at any step the server doesn't return the necessary headers, the browser will reject the request.
CORS Errors
Developers can see CORS errors only through the browser console - JavaScript code, in case of policy violations, receives only a generic network error. In the console, it will be indicated which header is missing or what exactly was blocked by the policy (Origin, method, header, etc.). To resolve the problem, you need to correctly configure the headers on the server.
For example, errors of this nature can occur:
Okay, we've seen how the browser decides "to allow or not." But what other methods of cross-origin interaction exist and why shouldn't they be confused with CORS? Let's discuss that next.
Alternatives and Related Mechanisms
Besides the discussed SOP and CORS, the following mechanisms can also be mentioned:
Access-Control-Request-Private-Network: true
The local server (router) must respond with the header:
Access-Control-Allow-Private-Network: true
otherwise the browser blocks the connection. This measure aims to prevent attacks where attackers used the victim's browser for unauthorized access to devices on their local network.
What exactly should we take away from all this? Next, we'll formulate key points and a minimal checklist.
Summary and Practical Conclusions
The Same-Origin Policy has been the foundation of web security for almost 30 years. Thanks to SOP, our browsers isolate tabs and frames from each other, preventing sites from stealing each other's data. At the same time, the modern web is impossible without the integration of different services and this is where CORS comes in handy. This mechanism carefully extends SOP, allowing safe data exchange between trusted domains. To work effectively with CORS, a developer needs to understand which headers to configure on the server and why the browser blocks a particular request. To summarize, let's note the key points:
Understanding SOP and CORS will allow you to work confidently with APIs, avoid annoying "Blocked by CORS" errors, and protect user data from most simple attacks on the web. This is mandatory knowledge for every web developer.
Author: Bair Ochirov
You can also find this article on our website - https://www.byteminds.co.uk/blog/understanding-cors-a-practical-guide