Back Arrow
From the blog

Understanding CORS: A Practical Guide

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.

Bair Ochirov

Front-end developer

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.

ORIGIN

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):

  1. A user visits and requests resources located at https://www.a.com.
  2. The loaded resources from https://www.a.com in the user's browser then initiate a request for resources located at https://www.b.com.
  3. 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.

Without SOP

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):

  1. The user logs into their bank at bank.com (a session cookie is saved in the browser).
  2. They then visit a malicious website, bad-site.com, which contains a hidden malicious script.
  3. This script initiates a request with the session cookies to bank.com on the user's behalf.
  4. 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.

With SOP

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.com cannot access the DOM, cookies, localStorage, or other data of a page on site-b.com.
  • Frame Isolation. If an <iframe src="..."> from a foreign domain is embedded in a page, the parent script cannot access iframe.contentWindow.document of 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

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.

CORS Policy

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:3000 to another origin http://localhost:4000, if the PUT method is not allowed, an error like this will appear in the console:
  • When requesting from origin http://localhost:3000 to another origin http://localhost:4000, if the value of the Access-Control-Allow-Credentials header is not set to true:
  • When requesting from origin http://localhost:3000 to another origin http://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.com and bbb.example.com could both execute a script assigning document.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.com can send a message to an embedded iframe from domain-b.com by calling iframe.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/socket the 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, like SharedArrayBuffer, 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 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.

Conclusion

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.

It's easy to start working with us. Just fill the brief or call us.

Find out more
White Arrow
From the blog
Related articles

How I Started Writing Unit Tests for Vue Components - Part 2

Dmitry Simonov

So, it's been a year since the last article, and a lot has changed. In this one, we're going to talk about integrating with Mock Service Worker (MSW).

vuejs
Vue

Performance Issues in Web Services: A Practical Guide to Identification and Resolution

Dmitry Bastron

Performance is not a feature to add later, but a core requirement as vital as security. This guide provides a structured approach to building performance into your web services from the start, ensuring they are fast and scalable by design.

Development
WebDev

Setting SMART Goals for Digital Product Success

Andrey Stepanov

This quick guide helps you define clear objectives and track progress effectively—ensuring every milestone counts.

Development

Building Teams for Digital Products: Essential Roles, Methods, and Real-World Advice

Andrey Stepanov

A digital product isn’t just about features or design—it’s about teamwork. In this article, we break down the essential roles in digital product development.

Development

Goals in Digital Development: How to Launch a Digital Product Without Failure

Andrey Stepanov

Essential tips for creating a development team that works toward product goals, not just tasks.

Development

How to Become a Kentico MVP

Dmitry Bastron

Hi, my name is Dmitry Bastron, and I am the Head of Development at ByteMinds. Today, I’d like to share how I achieved Kentico MVP status, why I chose this CMS, and what it takes to follow the same path and earn the coveted MVP badge.

Kentico

How can a team lead figure out a new project when nothing is clear?

Maria Serebrovskaya

Hello! My name is Maria, and I am a team lead and backend developer at ByteMinds. In this article, I will share my experience: I’ll explain how to understand a complex project, establish processes, and make it feel like "your own".

Development

How I Started Writing Unit Tests for Vue Components

Dmitry Simonov

In this article, we’ll take a look at how you can start testing Vue components.

Vue
vuejs

Inspecting raw database data in Xperience by Kentico

Dmitry Bastron

This article is a cheat sheet to inspect what's going on with the imported data by Xperience by Kentico Migration Toolkit, resolve some CI/CD issues, and on many other occasions!

Kentico

Learnings from using Sitecore ADM

Anna Bastron

Let's try to understand how the ADM module works, its limitations, and tactics for optimising its performance.

Sitecore

Your last migration to Xperience by Kentico

Dmitry Bastron

The more mature Xperience by Kentico products become, the more often I hear "How can we migrate there?”

Kentico

5 Key Software Architecture Principles for Starting Your Next Project

Andrey Stepanov

In this article, we will touch on where to start designing the architecture and how to make sure that you don’t have to redo it during the process.

Architecture
Software development

Assessing Algorithm Complexity in C#: Memory and Time Examples

Anton Vorotyncev

Today, we will talk about assessing algorithm complexity and clearly demonstrate how this complexity affects the performance of the code.

.NET

Top 8 B2B Client Service Trends to Watch in 2024

Tatiana Golovacheva

The development market today feels like a race - each lap is quicker, and one wrong move can cost you. In this race, excellent client service can either add extra points or lead to a loss due to high competition.

Customer Service
Client Service

8 Non-Obvious Vulnerabilities in E-Commerce Projects Built with NextJS

Dmitry Bastron

Ensuring security during development is crucial, especially as online and e-commerce services become more complex. To mitigate risks, we train developers in web security basics and regularly perform third-party penetration testing before launch.

Next.js
Development

How personalisation works in Sitecore XM Cloud

Anna Bastron

In my previous article, I shared a comprehensive troubleshooting guide for Sitecore XM Cloud tracking and personalisation. This article visualises what happens behind the scenes when you enable personalisation and tracking in your Sitecore XM Cloud applications.

Sitecore

Server and client components in Next.js: when, how, and why?

Sergei Pestov

All the text and examples in this article refer to Next.js 13.4 and newer versions, in which React Server Components have gained stable status and become the recommended approach for developing applications using Next.js.

Next.js

How to properly measure code speed in .NET

Anton Vorotyncev

Imagine you have a solution to a problem or a task, and now you need to evaluate the optimality of this solution from a performance perspective.

.NET

Formalizing API Workflow in .NET Microservices

Artyom Chernenko

Let's talk about how to organize the interaction of microservices in a large, long-lived product, both synchronously and asynchronously.

.NET

Hidden Aspects of TypeScript and How to Resolve Them

Dmitry Berdnikov

We suggest using a special editor to immediately check each example while reading the article. This editor is convenient because you can switch the TypeScript version in it.

TypeScript

Troubleshooting tracking and personalisation in Sitecore XM Cloud

Anna Gevel

One of the first things I tested in Sitecore XM Cloud was embedded tracking and personalisation capabilities. It has been really interesting to see what is available out-of-the-box, how much flexibility XM Cloud offers to marketing teams, and what is required from developers to set it up.

Sitecore

Mastering advanced tracking with Kentico Xperience

Dmitry Bastron

We will take you on a journey through a real-life scenario of implementing advanced tracking and analytics using Kentico Xperience 13 DXP.

Kentico
Devtools

Why is Kentico of such significance to us?

Anastasia Medvedeva

Kentico stands as one of our principal development tools. We believe it would be fitting to address why we opt to work with Kentico and why we allocate substantial time to cultivating our experts in this DXP.

Kentico

Where to start learning Sitecore - An interview with Sitecore MVP Anna Gevel

Anna Gevel

As a software development company, we at Byteminds truly believe that learning and sharing knowledge is one of the best ways of growing technical expertise.

Sitecore

Sitecore replatforming and upgrades

Anastasia Medvedeva

Our expertise spans full-scale builds and support to upgrades and replatforming.

Sitecore

How we improved page load speed for a Next.js e-commerce website by 50%

Sergei Pestov

How to stop the decline of the performance indicators of your e-commerce website and perform optimise page load performance.

Next.js

Sitecore integration with Azure Active Directory B2C

Dmitry Bastron

We would like to share our experience of integrating Sitecore 9.3 with Azure AD B2C (Azure Active Directory Business to Consumer) user management system.

Sitecore
Azure

Dynamic URL routing with Kontent.ai

We'll consider the top-to-bottom approach for modeling content relationships, as it is more user-friendly for content editors working in the Kontent.ai admin interface.

Kontent Ai

Headless CMS. Identifying Ideal Use Cases and Speeding Up Time-to-Market

Andrey Stepanov

All you need to know about Headless CMS. We also share knowledge about the benefits of Headless CMS, its pros and cons.

Headless CMS

Enterprise projects: what does a developer need to know?

Fedor Kiselev

Let's talk about what enterprise development is, what nuances enterprise projects may have, and which skills you need to acquire to successfully work within the .NET stack.

Development

Fixed Price, Time & Materials, and Retainer: How to Choose the Right Agreement for Your Project with Us

Andrey Stepanov

We will explain how these agreements differ from one another and what projects they are suitable for.

Customer success

Sitecore Personalize: tips & tricks for decision models and programmable nodes

Anna Gevel

We've collected various findings around decision models and programmable nodes working with Sitecore Personalize.

Sitecore

Umbraco replatforming and upgrades

Anastasia Medvedeva

Our team boasts several developers experienced in working with Umbraco, specialising in development, upgrading, and replatforming from other CMSs to Umbraco.

Umbraco

Kentico replatforming and upgrades

Anastasia Medvedeva

Since 2015, we've been harnessing Kentico's capabilities well beyond its core CMS functions.

Kentico

Interesting features of devtools for QA

Egor Yaroslavcev

Chrome DevTools serves as a developer console, offering an array of in-browser tools for constructing and debugging websites and applications.

Devtools
QA

Activity logging with Xperience by Kentico

Dmitry Bastron

We'll dive into practical implementation in your Xperience by Kentico project. We'll guide you through setting up a custom activity type and show you how to log visitor activities effectively.

Kentico
This website uses cookies. View Privacy Policy.