Web Security

What Is CORS and How Does It Work?

A clear guide to CORS and the same-origin policy: simple vs preflight requests, the Access-Control headers, credentials, the wildcard pitfall, and risks.

StackOptic Research Team18 May 20268 min read
What CORS is and how it works

CORS is one of the most misunderstood corners of web security — often blamed for errors it is merely reporting, and frequently mis-configured in ways that quietly undermine security. To understand it, you first have to understand the rule it relaxes. In short: the same-origin policy stops a page on one origin from reading responses from another origin, and CORS (Cross-Origin Resource Sharing) is the controlled, opt-in way for a server to say "these specific other origins are allowed". This guide explains the same-origin policy, how CORS actually works, the difference between simple and preflight requests, how credentials change the rules, the misconfigurations that matter, and how to inspect it all in DevTools.

It is a natural companion to how to read a website's HTTP headers and to the defensive grounding in how to protect your website from common attacks.

Answer first: the same-origin policy

Browsers enforce a foundational security rule called the same-origin policy. An origin is the combination of scheme + host + port — so https://app.example.com is a different origin from http://app.example.com (different scheme), from https://api.example.com (different host), and from https://app.example.com:8443 (different port). The policy says, in essence: a page from one origin may not read responses from a different origin.

Why does this exist? Because without it, a malicious site you happened to visit could use your browser — and your existing logged-in sessions — to silently read your data from other sites. Imagine evil.example making a request to your webmail or your bank and reading the response while you are logged in. The same-origin policy is what stops that. It is one of the most important security boundaries on the web.

But modern applications legitimately need to talk across origins — a front-end on app.example.com calling an API on api.example.com, for instance. CORS is the standardised, opt-in mechanism that lets a server selectively grant that cross-origin access without throwing the same-origin policy out entirely.

How CORS actually works

The crucial mental model: CORS is the server granting permission, and the browser enforcing it. When a page makes a cross-origin request, the browser adds an Origin header naming the requesting origin. The server inspects that and responds with Access-Control-* headers stating what it permits. The browser then reads those headers and decides whether to let the calling page access the response. If the server has not granted permission, the browser blocks the page from reading the response and reports a CORS error.

Two things follow from this that trip people up:

  • CORS is enforced by the browser, for browsers. A server-to-server request, or a curl from a terminal, is not subject to CORS at all — there is no browser to enforce it. CORS protects users' browsers, not the server.
  • A CORS error usually means the server did not send the right headers, not that the browser is broken. The browser is correctly enforcing the absence of permission.

Simple requests vs preflight requests

Not all cross-origin requests behave the same way. The specification distinguishes simple requests from those that require a preflight.

A simple request is one that meets a narrow set of conditions — broadly, it uses a method like GET, HEAD or POST, and only uses request headers and content types considered "safe". For these, the browser sends the request directly, includes the Origin header, and checks the response's Access-Control-Allow-Origin afterwards. If the origin is not allowed, the response is withheld from the page.

A request that falls outside those conditions — for example, one using PUT, PATCH or DELETE, or sending a custom header (like Authorization in some cases, or an application-specific header), or a non-simple content type — triggers a preflight. The browser automatically sends an OPTIONS request first, before the real one, to ask permission. This preflight carries:

  • Access-Control-Request-Method — the method the real request will use.
  • Access-Control-Request-Headers — any non-simple headers the real request will include.

The server responds with what it allows (Access-Control-Allow-Methods, Access-Control-Allow-Headers, and so on). Only if the preflight response permits it does the browser then send the actual request. If not, the real request is never sent, and the browser reports the failure. Preflights can be cached for a period via Access-Control-Max-Age to avoid repeating the round-trip.

This is why you sometimes see a mysterious OPTIONS request in the Network tab before your real PUT or DELETE — that is the preflight doing its job.

The CORS headers and their roles

Header (response)Role
Access-Control-Allow-OriginWhich origin(s) may access the response — a specific origin or *
Access-Control-Allow-MethodsWhich HTTP methods are permitted (preflight)
Access-Control-Allow-HeadersWhich request headers are permitted (preflight)
Access-Control-Allow-CredentialsWhether credentials (cookies, auth) may be included — true or absent
Access-Control-Expose-HeadersWhich response headers the page is allowed to read
Access-Control-Max-AgeHow long the preflight result may be cached
Origin (request)Set by the browser to the requesting origin
Access-Control-Request-Method (request)The intended method, sent in a preflight
Access-Control-Request-Headers (request)The intended headers, sent in a preflight

The Mozilla Developer Network (MDN) documents each of these precisely and is the authoritative reference when configuring a server.

Credentials change the rules

By default, cross-origin requests made by JavaScript do not include credentials — cookies, HTTP authentication, or client certificates. To send them, the front-end must opt in (for example, credentials: 'include' with the Fetch API), and the server must explicitly allow it. When credentials are involved, two stricter rules apply:

  1. The server must respond with Access-Control-Allow-Credentials: true.
  2. The server must name a specific origin in Access-Control-Allow-Originthe wildcard * is forbidden in combination with credentials.

The reasoning is exactly the same-origin concern from the start of this article: if any origin (*) could make credentialed requests and read the responses, then any malicious site could read a logged-in user's private data. The browser refuses that combination outright. This restriction is not an inconvenience to work around — it is a core safety property.

CORS misconfigurations that matter

Because CORS relaxes a security boundary, getting it wrong has real consequences. The common, dangerous mistakes:

  • Wildcard with credentials. The single most serious error is effectively allowing credentialed access from any origin. Browsers block the literal *-with-credentials combination, but developers sometimes recreate the danger by reflecting the request's Origin back in Access-Control-Allow-Origin and sending Access-Control-Allow-Credentials: true. This dynamically allows every origin to make credentialed requests — exposing authenticated data to any malicious site. Never reflect arbitrary origins when credentials are allowed.
  • Reflecting the Origin without validation. Echoing whatever Origin arrives, with no allow-list, trusts every site on the internet. Even without credentials this can leak data that should be origin-restricted.
  • Overly broad allow-lists or weak matching. Permitting more origins than necessary, or matching origins with sloppy string checks (so evil-example.com matches a rule meant for example.com), widens exposure.
  • Treating CORS as authentication. CORS controls which origins a browser will let read a response; it is not an access-control or authentication mechanism for your API. A non-browser client ignores it entirely. Your API still needs proper authentication and authorisation independent of CORS.
  • Over-permissive headers/methods. Allowing all methods and headers when only a couple are needed grants more than required.

The guiding principle mirrors the rest of security: be explicit and minimal. Maintain an allow-list of the specific origins that genuinely need access, permit only the methods and headers actually used, and only enable credentials for origins you fully trust — and even then, never with a wildcard or unchecked reflection.

How to inspect CORS in DevTools

CORS behaviour is fully visible in the browser, which makes diagnosing it straightforward:

1. The Network tab. Open DevTools → Network and find the cross-origin request. Click it and read the Response Headers for Access-Control-Allow-Origin and the other Access-Control-* values. If the request was non-simple, you will see a preceding OPTIONS preflight request — inspect its request and response headers to see what was asked and what was allowed.

2. The Console. When the browser blocks a request for CORS reasons, it prints a clear, specific error in the Console explaining which check failed (for example, "No 'Access-Control-Allow-Origin' header is present" or a credentials/wildcard conflict). This is usually the fastest way to understand why a cross-origin call is failing — read the message rather than guessing.

3. From a terminal. Because curl is not a browser, it will not enforce CORS, but you can still observe the headers a server returns by sending a request with an Origin header and reading the Access-Control-* response headers. This is useful for confirming what the server is configured to send. For the general technique, see how to read a website's HTTP headers.

Broader site audits can surface cross-origin and header configuration as part of a wider report; StackOptic, for instance, records the headers a site exposes alongside performance and security signals, giving context to what you find.

A worked mental example

Suppose your front-end at https://app.example.com calls https://api.example.com/orders with a DELETE and an Authorization header, including credentials. Step by step:

  1. The browser sees a non-simple request and sends an OPTIONS preflight to api.example.com with Origin: https://app.example.com, Access-Control-Request-Method: DELETE, and Access-Control-Request-Headers: authorization.
  2. The API responds with Access-Control-Allow-Origin: https://app.example.com, Access-Control-Allow-Methods: DELETE, Access-Control-Allow-Headers: authorization, and Access-Control-Allow-Credentials: true — naming the specific origin, not *, because credentials are involved.
  3. The browser sees permission granted and sends the real DELETE request with credentials.
  4. The API responds, and the browser lets the page read the response.

If the API had instead returned Access-Control-Allow-Origin: * while credentials were included, the browser would have blocked it — correctly. That is CORS protecting the user.

A quick CORS checklist

  • Default to the same-origin policy; open up cross-origin access only where needed.
  • Maintain an explicit allow-list of origins — never blindly reflect the Origin header.
  • Allow only the methods and headers your application actually uses.
  • For credentialed requests, set Access-Control-Allow-Credentials: true with a specific origin, never *.
  • Remember CORS is not authentication — secure your API independently.
  • Inspect the request in DevTools Network, watch for the preflight, and read Console errors when blocked.

Go deeper

Want a quick read on the headers and configuration a site exposes? Analyse any URL with StackOptic — security, performance and SEO in one free report.

Frequently asked questions

What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser mechanism that lets a server explicitly allow web pages from other origins to access its resources, relaxing the default same-origin policy in a controlled way. The server sends Access-Control-* response headers stating which origins, HTTP methods and request headers it permits; the browser reads them and either allows the page to access the response or blocks it. CORS is about a server granting permission, enforced by the browser.

What is the same-origin policy?

The same-origin policy is a core browser security rule: a document or script loaded from one origin (the combination of scheme, host and port) cannot read responses from a different origin. It exists so that a malicious site you visit cannot use your browser to read your data from other sites where you are logged in. CORS is the standardised way for a server to opt into sharing specific resources across origins despite this default restriction.

What is a CORS preflight request?

A preflight is an automatic OPTIONS request the browser sends before certain cross-origin requests, to ask the server's permission. It is triggered by requests that are not simple — for example, those using methods like PUT or DELETE, or custom headers. The browser sends the preflight with Access-Control-Request-Method and -Headers; the server responds with what it allows. Only if the response permits the actual request does the browser then send it. Simple requests skip this step.

Why can't I use a wildcard with credentials in CORS?

When a request includes credentials such as cookies or an Authorization header, the browser requires the server to respond with Access-Control-Allow-Credentials: true and to name a specific origin in Access-Control-Allow-Origin — the wildcard '*' is explicitly disallowed in this case. The reason is safety: allowing any origin to make credentialed requests and read the responses would let any malicious site read a logged-in user's private data, defeating the same-origin policy.

How do I check a site's CORS configuration?

Open browser developer tools, go to the Network tab, and inspect the cross-origin request. Look at the response headers for Access-Control-Allow-Origin and related Access-Control-* values, and watch for a preceding OPTIONS preflight request. The DevTools Console also prints a clear error when a request is blocked by CORS, naming the policy that failed. From a terminal you can send a request with an Origin header and read which Access-Control headers come back.

Analyse any website with StackOptic

Get the full technology stack, performance, security and SEO report in seconds — free.

Analyse a website

Related articles