Understanding CORS: A Practical Guide - Part 1

Understanding CORS: A Practical Guide - Part 1

posted Originally published at www.byteminds.co.uk 6 min read

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.

Author: Bair Ochirov

1 Comment

0 votes

More Posts

Understanding CORS: A Practical Guide - Part 2

ByteMinds - Nov 28

Mastering Trace Analysis with Span Links using OpenTelemetry and Signoz (A Practical Guide, Part 1)

NOIBI ABDULSALAAM - Oct 24, 2024

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

ByteMinds - May 29

How to Become a Kentico MVP - Interview with Kentico MVP Dmitry Bastron

ByteMinds - Mar 29

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

ByteMinds - Mar 4
chevron_left