Understanding CORS: A Practical Guide - Part 2

Understanding CORS: A Practical Guide - Part 2

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

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 favorable 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 frontend 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 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 image 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.

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

1 Comment

0 votes

More Posts

Understanding CORS: A Practical Guide - Part 1

ByteMinds - Nov 28

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

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

ByteMinds - Nov 21
chevron_left