Cross-Origin Resource Sharing (CORS)

What to do if you encounter CORS π«
Find the backend (β)
Also known as cross-origin resource sharing, when we use JavaScript to retrieve resources, non-same-origin requests are restricted for security reasons. Browsers enforce CORS compliance; otherwise, the request will fail.
What is the same origin?
According to the Same Origin Policy, two web pages are considered to have the same origin if they share the same protocol, port (if specified), and domain. Requests from websites that do not share the same origin are considered cross-origin HTTP requests. Compare https://www.terminal-420.space to the following URLs:
1. http://www.terminal-420.space // Different protocol, non-same origin
2. https://www.terminal-420.com // Different domain, non-same origin
3. https://www.cannabis.terminal-420.space // Different domain, non-same origin
4. https://www.terminal-420.space:3080 // Different port number, non-same origin
5. https://www.terminal-420.space/blog // Same origin
What is CORS?
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to manage and securely respond to requests from different origins.
In JavaScript, all non-same-origin requests are blocked unless otherwise configured on the server. However, the CORS specification categorizes cross-origin requests into two types: simple and preflighted.
Simple Request
A simple request is a request that can be sent directly without being preflighted (or without triggering preflighting). It is referred to as a non-preflighted request in the Fetch spec. Only the following three HTTP methods are allowed:
- GET
- HEAD
- POST
In addition to HTTP methods, simple requests can only contain specific header fields. In addition to the headers automatically set by the user agent (such as Connection, User-Agent), you can also add:
AcceptAccept-Language(en-US)Content-Language(en-US)Content-Type(note the additional requirements below)Last-Event-IDDPRSave-DataViewport-WidthWidth
Content-Type can only include application/x-www-form-urlencoded, multipart/form-data, and text/plain. Therefore, the following request does not meet the requirements for a simple request because Content-Type is application/json.
const response = await fetch('https://othersite.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});If the request meets all the requirements for a simple request, the browser will send a request to the server. If we send a request from https://www.terminal-420.space to another server, it might look like this:
GET /resources/public-data/ HTTP/1.1
Host: someserver.com
User-Agent: ...
Accept: ...
Accept-Language: ...
Accept-Encoding: ...
Accept-Charset: ...
Connection: ...
Referer: ...
Origin: https://www.terminal-420.space
The Origin header at the end indicates where the request originated. In this case, https://www.terminal-420.space is making the request. Let's see how the server responds to this request.
http HTTP/1.1 200 OK Date: ... Server: ... Access-Control-Allow-Origin: * Keep-Alive: ... Connection: ... Transfer-Encoding: ... Content-Type: application/xml
The header returned by the server contains an Access-Control-Allow-Origin header with a value of *, which allows any website to request cross-site resources. If the server only allows access from specific origins, such as https://www.terminal-420.space, the Access-Control-Allow-Origin header will be returned:
Access-Control-Allow-Origin: https://www.terminal-420.space
Origins other than https://www.terminal-420.space will be unable to access the resource.
Preflighted Requests
All requests other than simple requests must be preflighted before the actual request can be sent. After the request is sent, it will undergo a CORS check. When several conditions are met (one of which is setting the use-CORS-preflight flag ), a preflight request (CORS-preflight fetch) is sent.
A CORS-preflight fetch is sent using the HTTP OPTIONS method to confirm whether the server can accept the request. A preflighted request might look like this:
http OPTIONS /resources/post-here/ HTTP/1.1 Host: bar.example Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: https://foo.example Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type
The Access-Control-Request-Method and Access-Control-Request-Headers fields at the bottom contain the actual request method and headers, respectively. Here, the request we actually want to send is a POST, which includes the X-PINGOTHER and Content-Type headers.
If all of these match the server's settings, the server will return a 200 OK response.
http HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: https://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400 Vary: Accept-Encoding, Origin Keep-Alive: timeout=2, max=100 Connection: Keep-Alive
This includes:
Access-Control-Allow-Origin: Origins that the server allows for CORS requestsAccess-Control-Allow-Methods: Request methods allowed by the server, includingPOST,GET, andOPTIONS.
Access-Control-Allow-Headers: Headers allowed by the server.
Access-Control-Max-Age: The cached lifetime for these permissions, which is 86400 seconds, or one day.
According to MDN, the status codes 200 and 204 No Content are both permitted. However, some browsers seem to refuse to send subsequent requests due to a 204 status code.
Request with credentials
By default, cross-origin Fetch or XMLHttpRequest requests do not include cookies unless a specific flag is passed in the request. In Fetch, credentials needs to be set to include.
fetch('https://someserver.com/resources/public-data/', {
credentials: 'include'
})And in XMLHttpRequest, withCredentials = true needs to be passed.
const invocation = new XMLHttpRequest();
const url = "https://bar.other/resources/credentialed-content/";
function callOtherDomain() {
if (invocation) {
invocation.open("GET", url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}Also, whether it's a simple request or a request requiring preflight, if the server doesn't include Access-Control-Allow-Credentials: in the response, If true is set, the browser will still not see the returned information.
CORS-preflight cache
The CORS-preflight cache is a list containing cached entities. Each CORS-preflight cache entry contains:
- key (a network partition key)
- byte-serialized origin (a byte sequence)
- URL (a URL)
- max-age (a number of seconds)
- credentials (a boolean)
- method (null,
*, or a method) - header name (null,
*, or a header name)
Once the cache entry is created, it is added to the user The agent's CORS-preflight cache is stored in the proxy.
To remove the cache, the key, byte-serialized origin, and URL are compared. The correct cache is found and then removed.
What does CORS do in Express?
Now that we finally understand CORS, let's take a look at what CORS does in Express. cors is a middleware package for Express that sets CORS parameters.
Opening the cors source code reveals that it's only a little over 200 lines long, and the purpose of each function is quite clear. Different functions are used to set different headers and store them in arrays. Finally, join is used to combine the values in the arrays, or the array is returned for further combination.
For example, configureMethods for setting methods:
function configureMethods(options) {
var methods = options.methods;
if (methods.join) {
methods = options.methods.join(','); // .methods is an array, so turn it into a string
}
return {
key: 'Access-Control-Allow-Methods',
value: methods
};
}Looking back at the settings in express, it's easy to understand.
const express = require('express')
const app = express()
const cors = require('cors')
app.use(cors({
origin: "http://127.0.0.1:5500",
methods: ["GET", "POST"],
credentials: true
}))Accessing Private Networks - RFC1918 CORS
To prevent cross-site request forgery (CSRF) attacks from reaching other devices on a private or local network via a home router, Google proposed CORS for private networks (RFC1918). The most significant change is that starting with Chrome version 94, requests to private networks from insecure (HTTP) websites will no longer be allowed.
The access types affected include:
- Requests from the public network to a private network
- Requests from the public network to a local network
- Requests from a private network to a local network
So... What is private network access?
Private network requests are requests whose target server's IP address is more private than the one from which the request initiator was fetched. For example, a request from a public website (https://example.com) to a private website (http://router.local), or a request from a private website to localhost.
To understand private network access, we must first define local, private, and public networks.
The local IP address space includes the IPv4 loopback addresses 127.0.0.0/8 and the IPv6 loopback addresses 0:0:0:0:0:0:0:1 (or ::1/128).
The private IP address space includes IP addresses that are meaningful only within a specific domain or network segment, such as a router's internal network. Examples include 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16.
Finally, networks outside these categories are considered public.
A loopback address is an IP address that allows a system to send messages to itself. It can be used to verify that the TCP/IP stack is installed and functioning properly. It is essentially localhost. All IPv4 IP addresses beginning with 127 are loopback addresses.
Of the three types of IP addresses, the local network is the most private, followed by the private network, and finally the public network. The relationship between the three is shown in the figure below.

How does this relate to CORS?
This new change adds two preflight headers: Access-Control-Request-Private-Network and Access-Control-Allow-Private-Network.
After this change, any request to a private network must first be preceded by a preflight request with the Access-Control-Request-Private-Network: true header, regardless of the request method or mode. If the server allows the request, the response must include the Access-Control-Allow-Private-Network: true header.
In addition, if a less private network (such as a private IP address) wants to make a request to a more private network (such as a regional IP address), even if the request is in same-origin mode, a preflight request must be sent before the normal request can be made.
Let's look at some practical examples
In the URL https://foo.example/index.html, there's an image tag <img src="https://bar.example/cat.gif" alt="dancing cat"/>. The image's location, domain bar.example, resolves to the IP address 192.168.1.1, which is on a private network. Therefore, this request is a request from the "public network" to the "private network."
According to the new specification mentioned above, Chrome will send a preflight request like this, with the new header Access-Control-Request-Private-Network: true.
HTTP/1.1 OPTIONS /cat.gif
Origin: https://foo.example
Access-Control-Request-Private-Network: trueFor this preflight request to succeed, the server must include the Access-Control-Allow-Private-Network: true header in the response.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Private-Network: trueFinally, after the preflight request succeeds, Chrome will send the actual request to return the image resource.
HTTP/1.1 GET /cat.gif
...Conclusion
What to do if you encounter CORS π«
Confidently contact the backend (β )
References
- Cross-Origin Resource Sharing (CORS)
- MDN - Preflight request
- Fetch Standard - CORS-preflight cache
- [Tutorial] Deep Dive into CORS (Cross-Origin Resource Sharing): How to Configure CORS Correctly?
- Feedback wanted: CORS for private networks (RFC1918)
- Private Network Access update: Introducing a deprecation trial
- Private Network Access: introducing preflights