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):
- A user visits and requests resources located at
https://www.a.com. - The loaded resources from
https://www.a.comin the user's browser then initiate a request for resources located athttps://www.b.com. - The browser checks the so-called Origin and, as seen in this case, the origins do not match, so the browser blocks access to the resources from
https://www.b.com.
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):
- The user logs into their bank at
bank.com(a session cookie is saved in the browser). - They then visit a malicious website,
bad-site.com, which contains a hidden malicious script. - This script initiates a request with the session cookies to bank.com on the user's behalf.
- As a result, the malicious script receives a response from the bank without your knowledge!
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:
Blocking Access to Page Content. A script cannot read or modify the content of a page from another domain. For example, JavaScript from
site-a.comcannot access the DOM, cookies,localStorage, or other data of a page onsite-b.com.Frame Isolation. If an
<iframe src="...">from a foreign domain is embedded in a page, the parent script cannot accessiframe.contentWindow.documentof that frame (and vice versa) access will be denied as long as their origins differ.Blocking Access to HTTP Responses via XHR/Fetch. The browser blocks receiving the response to AJAX requests sent by a script to another domain. That is, you can send a fetch to a third-party API, but if their Origins are different, the browser will not provide the response to the script.
Exceptions (Allowed Loads). SOP restricts access to data from third-party origins, not the loading of resources themselves. The browser can load images, styles, scripts, media, and frames from another domain without errors and use them as-is: display an image, apply a style, execute a script, play a video, render an iframe. However, the page's JavaScript is not allowed to read the internal content of these resources (image pixels, style rules, frame DOM, video bytes, etc.) unless the resource's server explicitly permits access via CORS.
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):
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:
Access-Control-Allow-Credentials– controls access to resources considering authorization. If this header is set to true, the browser will allow access to such a response. Important: when using Allow-Credentials: true, the Allow-Origin value cannot be * - you must explicitly specify the specific domain, otherwise the browser will ignore the response.Access-Control-Allow-Methods– a list of HTTP methods allowed when accessing the resource. If the script plans to send not only GET but, say, PUT or DELETE, the server must list them in this header, otherwise the browser will deny access.Access-Control-Allow-Headers– similarly, a list of non-standard headers allowed in the request. For example, if the frontend wants to send a header like X-Custom-Header or Authorization, the server must explicitly allow them via this header.Access-Control-Max-Age– the time (in seconds) for which the results of the preflight check can be cached. This header allows the browser to avoid making extra preflight checks (more on them later) for repeated requests within the specified time.Access-Control-Request-Method– a header sent in the preflight request (more on that later) informing the server of the intended method of the main request.Access-Control-Request-Headers– a header sent in the preflight request (more on that later) informing the server of the list of non-standard headers the client wants to send in the main request.
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:
- When requesting from origin
http://localhost:3000to another originhttp://localhost:4000, if the PUT method is not allowed, an error like this will appear in the console:
- When requesting from origin
http://localhost:3000to another originhttp://localhost:4000, if the value of the Access-Control-Allow-Credentials header is not set totrue:
- When requesting from origin
http://localhost:3000to another originhttp://localhost:4000, if the request header custom-header is not allowed:
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:
Bypassing SOP via document.domain. Historically, a workaround was devised for subdomains: pages
aaa.example.comandbbb.example.comcould both execute a script assigningdocument.domain = "example.com", and then the browser would consider them the same origin. However, this approach is now outdated and declared unsafe. For example, Chrome plans to completely disable the ability to set document.domain because it undermines SOP protection (link to MDN).Interaction via window.postMessage(). This API allows scripts from different origins to communicate safely. For example, a page from
domain-a.comcan send a message to an embedded iframe fromdomain-b.comby callingiframe.contentWindow.postMessage(data, targetOrigin). If the targetOrigin matches (or "*" is specified for any), then on the domain-b.com side, the iframe will catch the message event and can read the data. An important property - neither the parent nor the iframe gains access to the other's DOM or JS objects; they only exchange string messages. postMessage is the primary way to integrate between different applications within the same window/tab (e.g., between a payment widget and a website).JSONP (JSON with Padding). Before CORS became widespread, this was a popular trick for getting data from another domain. The gist: the site inserts a tag
<script src="https://other.com/data?callback=parser">into the page. The server returns JavaScript code that calls the global function parser(...) with the JSON data inside. Because the<script>tag is not blocked by SOP (the script will execute), the data "leaks" into the function call on the first site's side. Disadvantages of JSONP - it only works for GET requests and carries risks (execution of third-party code). Nowadays, JSONP is hardly used, having given way to CORS, which supports any methods and doesn't allow direct execution of foreign code.
WebSockets. Interestingly, for WebSocket connections, SOP in its usual form does not apply. A page from JS can attempt to connect to
wss://another-domain.com/socketthe browser will allow this. However: when establishing the WS connection, the browser still sends the Origin header in the handshake. The WebSocket server must check this header itself and decide whether to allow this origin. Otherwise, an attacker could bypass SOP and establish communication with a private server. Thus, security for WS is the responsibility of the server: the browser trusts it and does not block connection attempts.Resource Loading Control (CORP, COEP, COOP). New standards introduce additional headers to enhance isolation. For example, Cross-Origin Resource Policy (CORP) allows a server to declare that its resources (scripts, images, etc.) should not be loaded on third-party sites. If an image with CORP=same-site is attempted to be inserted via
<img>on a foreign site, the browser will block it entirely. This helps prevent side-channel attacks and information leakage through hidden resource inclusion.Cross-Origin Opener/Embedder Policy(COOP/COEP) - even more advanced headers used for isolating contexts (e.g., to enable shared memory sharing, likeSharedArrayBuffer, between tabs of the same site, they must be completely isolated from outsiders). These topics are beyond the scope of this overview, but mentioning them shows how the idea of controlling interaction between sites is evolving.Private Network Access (PNA). Browser developers continue to enhance security policies. For example, in 2022, the
Private Network Access(PNA) mechanism appeared - an extension of CORS for protecting local networks. Chrome was one of the first to implement PNA: now if a script on a website from the internet tries to access a resource in a private network, before the actual request, the browser will send a special preliminary request with the header ([Source link]):
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 unauthorised 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:
SOP blocks scripts from accessing foreign content. Don't trust solutions that try to disable SOP - in modern browsers, this is impossible without compromising security.
CORS is a tool in the hands of the server-side developer. By correctly setting the headers (Origin, methods, headers, credentials), you tell the browser: "this request can be trusted," and the browser will comply.
When debugging CORS issues, carefully look at the messages in the browser console - they will hint at which header is missing.
Always restrict access to the minimum necessary: specify concrete origins instead of *, allow only the necessary methods and headers. This reduces the chance of your API being abused.
Security is evolving: besides CORS, study other mechanisms (CSRF tokens, SameSite cookies, CSP, etc.) to build truly secure applications.
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













Top comments (0)