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.
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
curlfrom 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-Origin | Which origin(s) may access the response — a specific origin or * |
Access-Control-Allow-Methods | Which HTTP methods are permitted (preflight) |
Access-Control-Allow-Headers | Which request headers are permitted (preflight) |
Access-Control-Allow-Credentials | Whether credentials (cookies, auth) may be included — true or absent |
Access-Control-Expose-Headers | Which response headers the page is allowed to read |
Access-Control-Max-Age | How 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:
- The server must respond with
Access-Control-Allow-Credentials: true. - The server must name a specific origin in
Access-Control-Allow-Origin— the 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'sOriginback inAccess-Control-Allow-Originand sendingAccess-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
Originarrives, 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.commatches a rule meant forexample.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:
- The browser sees a non-simple request and sends an
OPTIONSpreflight toapi.example.comwithOrigin: https://app.example.com,Access-Control-Request-Method: DELETE, andAccess-Control-Request-Headers: authorization. - The API responds with
Access-Control-Allow-Origin: https://app.example.com,Access-Control-Allow-Methods: DELETE,Access-Control-Allow-Headers: authorization, andAccess-Control-Allow-Credentials: true— naming the specific origin, not*, because credentials are involved. - The browser sees permission granted and sends the real
DELETErequest with credentials. - 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
Originheader. - Allow only the methods and headers your application actually uses.
- For credentialed requests, set
Access-Control-Allow-Credentials: truewith 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
- Read the headers behind it: how to read a website's HTTP headers.
- The broader defensive picture: how to protect your website from common attacks.
- Harden the response further: HTTP security headers explained.
- Verify third-party assets: what is Subresource Integrity (SRI)?
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 websiteRelated articles
How to Check a Website for Malware
A practical guide to checking any website for malware: the free external scanners to use, the signs of infection, server-side checks, and what to do next.
What Is a Data Breach and How to Respond
A plain-English guide to data breaches: what counts as one, the common causes, a step-by-step incident-response plan, the GDPR 72-hour rule, and prevention.
How to Protect Your Website from Bots and Scrapers
Not all bots are bad. Tell good crawlers from abusive scrapers, spot the signals of bot traffic, and layer rate limiting, CAPTCHA, a WAF and bot management.