⌨️ Web development

CORS explained: why the browser is blocking your request

Same-origin policy, simple and preflight requests, Access-Control headers and credentials: how CORS works and how to configure it securely on the server.

Sooner or later it happens to everyone: your JavaScript calls an API, the request looks correct, but the console shows an error about "CORS policy" and the response never arrives. It is one of the most misunderstood messages in web development, partly because it is often "fixed" in ways that create security problems. Understanding the mechanism makes the solution almost obvious.

The same-origin policy

It all starts with a fundamental browser security rule: the same-origin policy. A script loaded from a given origin can freely read only responses coming from the same origin.

An origin is the combination of three elements: scheme, host and port.

Page URL Requested URL Same origin?
https://example.com https://example.com/api yes
https://example.com http://example.com/api no (scheme)
https://example.com https://api.example.com no (host)
https://example.com https://example.com:8443 no (port)

Without this rule, any site you visit could use your browser, with your cookies, to read your email or your online bank account from another tab.

What CORS is

CORS (Cross-Origin Resource Sharing) is the mechanism by which a server can declare: "I accept that pages from these other origins may read my responses". It is therefore a controlled way to relax the same-origin policy, not an extra protection.

A key point that is often overlooked: CORS is enforced by the browser. Tools such as curl or a server-side application are not subject to it. If the request works from a terminal but not from the browser, the problem is almost always CORS.

What is more, in many cases the request still reaches the server and is executed: it is the browser that, on receiving the response without the right headers, prevents the script from reading it. That is why CORS does not replace authentication or protection against unwanted requests.

Simple requests

Some requests are sent directly, with an Origin header indicating where they come from:

GET /api/articles HTTP/1.1
Host: api.example.com
Origin: https://www.example.com

If the server wants to allow that origin, it responds with:

Access-Control-Allow-Origin: https://www.example.com

If the header is missing or names another origin, the browser blocks the response from being read. "Simple" requests are GET, HEAD and POST with standard headers and a Content-Type of text/plain, application/x-www-form-urlencoded or multipart/form-data.

Preflight requests

A request using PUT or DELETE, with a custom header such as Authorization, or with Content-Type: application/json is not "simple". Before sending it, the browser asks for permission with an OPTIONS request, called a preflight:

OPTIONS /api/articles/42 HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization

The server must respond stating what it allows:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600

Only if the response is satisfactory does the browser send the real request. Access-Control-Max-Age lets the permission be cached for a number of seconds, avoiding a preflight on every call.

A typical server-side mistake is handling the route only for the expected methods and returning 404 or 405 to OPTIONS requests, or requiring authentication on the preflight too: the browser sends no credentials in the preflight, so the request fails.

Credentials and cookies

By default, cross-origin requests made with fetch do not include cookies. To include them:

fetch('https://api.example.com/profile', {
  credentials: 'include'
});

And the server must respond with:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true

In this case the wildcard * is not allowed for the origin: the server must explicitly name the permitted one. This rule is deliberate, to stop anyone from reading authenticated responses.

Configuring CORS securely

The most common, and most dangerous, mistake is reflecting whatever origin is received:

// Avoid: it amounts to allowing everyone
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');

With credentials enabled, this configuration lets any site read the private data of authenticated users. The correct approach is a list of allowed origins:

const allowed = new Set([
  'https://www.example.com',
  'https://admin.example.com'
]);

const origin = req.headers.origin;
if (allowed.has(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Vary', 'Origin');
}

The Vary: Origin header matters when the response changes depending on the origin: it tells caches not to serve one origin the response prepared for another.

A few more rules:

  • use * only for genuinely public resources without credentials, such as an open-data API or fonts;
  • do not compare origins with partial checks such as "contains example.com": example.com.malicious-site.net contains it;
  • allow only the methods and headers you need.

Alternatives to CORS

If both the frontend and the API are yours, the simplest solution is often to remove the need for CORS: serve the API on the same domain as the site, for example under /api, through a proxy or your host's rewrite rules. Requests become same-origin and the problem disappears.

In short

  • The same-origin policy stops scripts from reading responses from other origins (scheme, host, port).
  • CORS is how the server relaxes that rule, and it is enforced by the browser.
  • Non-simple requests require an OPTIONS preflight, which the server must handle without authentication.
  • With credentials you cannot use *: you need an explicit list of origins.
  • Never reflect the received origin without checking it, and add Vary: Origin.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also