An API that works after you add Access-Control-Allow-Origin is not necessarily safe. CORS isn't something you add once and forget; what matters is narrowing who you allow. Here's how it works and how to set it safely (no attack steps).
First, how it works
Browsers enforce a same-origin policy: JS from another origin can't freely read another site's responses. CORS exists to open that exception only to parties the server explicitly allows.
An origin is the combination of scheme (https), host name and port. https://example.com and https://api.example.com have different hosts, so they are different origins; http://example.com and https://example.com differ in scheme, so they are different too.
| Response header | Meaning |
|---|---|
Access-Control-Allow-Origin | Which origins may read the response |
Access-Control-Allow-Credentials | Whether requests with credentials (cookies) are allowed |
Access-Control-Allow-Methods / -Headers | Which HTTP methods/headers are allowed |
The point: CORS is on the "loosen the restriction" side. Loosen it wrong and data that shouldn't be readable becomes readable to third parties.
One more thing that matters: what CORS stops is JavaScript reading the response. A simple request reaches the server and is processed even without CORS permission. For cases like custom headers, the browser first checks with an OPTIONS request (a preflight) and doesn't send the real request if it isn't allowed.
How it differs from similar mechanisms
CORS is often confused with similarly named mechanisms and with a closely related vulnerability.
| Term | What it decides | Who enforces it |
|---|---|---|
| Same-origin policy | Other origins can't read responses (browser default) | The browser |
| CORS | As an exception to that, which origins may read | The server declares, the browser enforces |
| CSRF | An attack that makes the user's logged-in browser send unintended requests | Prevented with CSRF tokens and SameSite |
| CSP | Which scripts and connections your own page may load | The server declares, the browser enforces |
CORS is about "can the response be read"; CSRF is about "can a request be sent on the user's behalf." Tightening CORS doesn't prevent CSRF.
What makes a setting dangerous
Dangerous settings (common)
- Reflecting the request's Origin back as allowed
Access-Control-Allow-Origin: *plus allowing credentials- Opening APIs broadly cross-origin that don't need it
Safe settings
- An allowlist (only fixed, trusted origins)
- Default deny for everything else
- For credentialed requests, a specific origin, not
*
How misconfigurations usually happen
Most misconfigurations come from trying to make a CORS error go away. The typical sequence:
- Calling the API from the front end with cookies produces a CORS error
- You add
Access-Control-Allow-Origin: *. But for credentialed requests the browser won't let the page read a*response, so the error remains - You switch to echoing back the incoming
Origin, and the error disappears. At the same time, any site can now read logged-in responses
Even with an allowlist, a sloppy comparison leads to the same place.
- Matching "ends with
example.com" also lets in a different domain such asevil-example.com - Matching "starts with
https://example.com" also lets inhttps://example.com.evil.net - Allowing the
nullorigin for development convenience also allows reads fromnullorigins that other sites can produce
Compare the whole origin (scheme, host and port) for an exact match.
The basics of a safe setting
Use an allowlist (most important)
Don't blindly reflect Origin
Origin straight into Access-Control-Allow-Origin. Return it only when it matches the allowlist. Because the value now varies per request, also send Vary: Origin so caches don't mix responses up.Never use * with credentials
Access-Control-Allow-Origin: * with Allow-Credentials: true. Name a specific origin instead.Minimize what you open
* only for public data that needs no login.How to check your own site
Send your API a request that claims a different origin and look at the response headers. Do this only against APIs you manage.
# Claim an origin you haven't allowed, and look only at the response headers
curl -s -o /dev/null -D - -H "Origin: https://example.com" https://your-api.example/ | grep -i "^access-control"- If you get
Access-Control-Allow-Origin: https://example.com, the API reflects the Origin. Add allowlist matching - If both
Access-Control-Allow-Origin: *andAccess-Control-Allow-Credentials: trueappear, revisit the setting - If nothing comes back, or only your own front end's origin does, you're fine
Repeat the same check with Origin: null. For a site whose ownership you've verified, the site security audit checks origin reflection and credential settings together. Other security headers can be checked with the security headers checker.
This site's view: CORS is 'how you loosen,' not 'defense' — narrow who you open to
A common misconception is "adding CORS headers makes it safe." It's the opposite: CORS loosens the browser's restriction. So what matters isn't "how you add it" but "minimizing who, and how far, you open to." On this site we separate data that's fine to read externally from data that isn't, and recommend default deny, with an allowlist opening only the exceptions. Note that XSS (which breaks the read assumption) and CSRF (which targets state changes) are separate bugs — CORS alone doesn't cover everything.
Sources (primary)
- MDN Web Docs, "Cross-Origin Resource Sharing (CORS)" — developer.mozilla.org (no * with credentialed requests, preflight conditions, Vary: Origin)
- WHATWG Fetch Standard, "CORS protocol" — fetch.spec.whatwg.org
- OWASP HTML5 Security Cheat Sheet — cheatsheetseries.owasp.org (allow only trusted domains; don't return the Origin header unchecked)
- PortSwigger Web Security Academy, "Cross-origin resource sharing (CORS)" — portswigger.net (allowlist matching errors, the null origin, CORS is not CSRF protection)
- MITRE CWE-942: Permissive Cross-domain Policy with Untrusted Domains — cwe.mitre.org
Read next
- Glossary: What is XSS · What is CSRF · What is a JWT
- Learn: Choosing security headers
- Check: Security headers checker · Site security audit