When Access-Control-Allow-Origin: * Is Actually Fine
The asterisk. The wildcard. The star. The character that makes security-conscious developers break out in hives.
I'm here to tell you that Access-Control-Allow-Origin: * is a perfectly legitimate,
spec-compliant, and appropriate CORS configuration for a wide range of use cases.
It's been unfairly maligned by Stack Overflow answers, security scanners that don't
understand context, and that one coworker who read half a blog post about CORS and
now treats * like it's rm -rf /.
Let's talk about when it's fine, when it's not, and how to tell the difference.
The Asterisk Has Been Unfairly Maligned
Here's a sample of advice you'll find on the internet:
"Never use
Access-Control-Allow-Origin: *in production!""The wildcard is a security vulnerability!"
"Always specify explicit origins!"
This advice is well-intentioned and, in many cases, wrong. Or rather, it's advice
that's correct for some situations applied as if it's correct for all situations.
It's the CORS equivalent of "never use SELECT *" — a reasonable heuristic that
becomes cargo cult when applied without understanding.
The wildcard * means: "any origin may read this response." That's a security
problem when the response is different depending on who's asking — specifically,
when the server uses ambient credentials (cookies, HTTP auth) to personalize the
response. But many, many endpoints don't do that. For those endpoints, * is not
just fine — it's the correct choice.
When * Is Perfectly Appropriate
Public APIs with no authentication
If your API endpoint:
- Doesn't use cookies
- Doesn't use HTTP authentication
- Doesn't check the
Authorizationheader - Returns the same response to every requester
Then Access-Control-Allow-Origin: * is correct.
# A public API that returns weather data
curl -v https://api.weather.example.com/current?city=london
# Response:
# HTTP/2 200
# Access-Control-Allow-Origin: *
# Content-Type: application/json
#
# {"temp": 12, "conditions": "overcast", "unit": "celsius"}
This endpoint returns the same weather data regardless of who asks. There are no cookies, no sessions, no user-specific data. Restricting it to specific origins would be pure security theater — adding complexity and operational burden for zero security benefit.
Consider: if an attacker wanted this data, they'd just call the API from their server. CORS doesn't apply to server-to-server requests. The only thing restricting the origin would do is prevent other websites' frontend JavaScript from accessing your public data. Why would you want that? If the API is public, let it be public.
Open data endpoints
Government data portals, open datasets, public statistics — all of these serve the same data to everyone. There's no ambient authority involved.
# Good. Correct. Nothing to worry about.
Access-Control-Allow-Origin: *
Examples of real-world APIs that correctly use *:
- OpenStreetMap tile servers
- Public government APIs (data.gov, etc.)
- Wikipedia's API
- npm's registry API
- Public blockchain APIs
These are some of the highest-traffic APIs on the internet. They all use *. They're
not wrong.
CDN-served static assets
CSS files, JavaScript bundles, fonts, images — if they're served from a CDN and
intended to be used by any website, * is correct.
# Serving fonts from a CDN
GET /fonts/opensans.woff2 HTTP/2
Host: cdn.example.com
HTTP/2 200
Access-Control-Allow-Origin: *
Content-Type: font/woff2
Cache-Control: public, max-age=31536000
In fact, for fonts specifically, * is almost always what you want. Web fonts loaded
via @font-face are subject to CORS checks. If you're hosting fonts on a CDN for
others to use, restricting the origin means only specific sites can use your fonts.
That defeats the purpose of putting them on a CDN.
Public documentation APIs
Swagger/OpenAPI endpoints, API documentation, health check endpoints — all public,
all stateless, all safe with *.
# Health check endpoint. No secrets here.
GET /health HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json
{"status": "ok", "version": "2.3.1"}
Any endpoint where the response is truly public
The general rule: if you'd be comfortable with the response being cached by Google,
displayed on a billboard, or printed in a newspaper, * is fine. If the response
is public information, letting any origin read it doesn't change anything.
When * Is NOT Appropriate
Now let's talk about when * will get you into trouble.
Any endpoint that uses cookies or session authentication
If your endpoint reads cookies to determine who the user is, * is wrong. Not because
* itself is dangerous in this case, but because the natural next step is almost
always a mistake.
Here's the progression:
- Developer sets
Access-Control-Allow-Origin: * - Frontend sends
credentials: 'include' - Browser refuses (can't use
*with credentials) - Developer "fixes" it by echoing the origin blindly
And now you have the reflected origin vulnerability from Chapter 21. The * itself
wasn't the security hole — but it was the first step on a path that leads to one.
If your endpoint uses cookies, you need to:
- Validate the origin against an allowlist
- Echo back the validated origin
- Set
Access-Control-Allow-Credentials: true - Include
Vary: Origin
# Correct for a cookie-authenticated endpoint:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Vary: Origin
Any endpoint that relies on the Origin for access control
If your server checks the Origin header to decide whether to allow a request
(which, as discussed in Chapter 21, is fragile), * obviously doesn't work because
you're not checking the origin at all.
Internal APIs
APIs meant for internal use — between microservices, internal tools, admin panels —
should not have Access-Control-Allow-Origin: *. Not because * is dangerous per se
(internal APIs should have authentication regardless), but because:
- Internal APIs should not be accessible from arbitrary web pages
- If an internal API has no auth (which it shouldn't, but many do),
*would let any website interact with it from the user's browser - The principle of least privilege says: don't allow access you don't intend to grant
For internal APIs, either don't set CORS headers at all (blocking all cross-origin browser access) or allow only the specific internal origins that need access.
Any endpoint that uses Authorization headers
If your API requires an Authorization: Bearer <token> header, * is technically
fine as long as the token isn't sent via cookies. The token comes from JavaScript
code that explicitly adds it, not from the browser automatically attaching credentials.
However, I'd still recommend explicit origins here, because:
- It signals to other developers that you've thought about security
- It limits the blast radius if there's a token leakage
- It's no harder to configure than
*
The Real Question: Does Your Endpoint Use Ambient Authority?
Here's the framework that actually matters. Forget the specific rules about cookies and credentials modes for a moment. Ask yourself one question:
Does the browser automatically attach anything to the request that changes the response?
"Ambient authority" means credentials that the browser sends without the JavaScript code explicitly adding them:
- Cookies: The browser attaches them automatically based on the domain.
- HTTP authentication: The browser sends cached credentials automatically.
- Client certificates: The browser presents them automatically during TLS.
If the answer is "yes, the browser attaches something that gives the request access
it wouldn't otherwise have," then * is inappropriate. You need explicit origin
validation with credentials.
If the answer is "no, the request is the same regardless of which browser or which
page sends it," then * is fine. The data is publicly accessible, and CORS is not
adding any meaningful security boundary.
# Decision tree in pseudocode:
if endpoint_uses_cookies or endpoint_uses_http_auth:
use explicit_origin_allowlist
set Access-Control-Allow-Credentials: true
elif endpoint_requires_bearer_token:
# Token is in header, not ambient. * works but explicit is better.
prefer explicit_origin_allowlist
# * is acceptable if the API is intended to be public
else:
# No auth, same response for everyone
use Access-Control-Allow-Origin: *
If the Response Is the Same Regardless of Who's Asking
Let me make this concrete. Consider two endpoints:
Endpoint A: Public stock price
curl https://api.example.com/stock/AAPL
# {"symbol": "AAPL", "price": 187.42, "currency": "USD"}
# Same response whether it's your browser, my browser, or curl.
# No cookies needed. No auth needed. Public data.
# Access-Control-Allow-Origin: * is correct.
Endpoint B: User's portfolio
curl -H "Cookie: session=abc123" https://api.example.com/portfolio
# {"holdings": [{"symbol": "AAPL", "shares": 100}, ...]}
# Response depends on the session cookie. Different user, different data.
# The browser attaches this cookie automatically.
# Access-Control-Allow-Origin: * is WRONG (and won't work with credentials anyway).
See the difference? It's not about whether * is inherently good or bad. It's about
whether the security boundary that * removes was doing anything useful.
If the Response Depends on Ambient Credentials, * Is Dangerous
Let's be specific about why it's dangerous. With *, the browser won't send cookies
cross-origin (because credentials: 'include' is incompatible with *). So you
might think: "well, * is safe because the browser won't send cookies anyway."
That's technically true — but it creates a false sense of security. Someone will
eventually change the CORS config to support credentials (because a new feature
requires it), and they'll do it wrong (see Chapter 21, reflected origin attacks).
Starting with * on an authenticated endpoint is planting a landmine for your future
colleagues.
Also, there are edge cases where * on an authenticated API can leak information:
-
If the endpoint returns different data based on the
Authorizationheader (which JavaScript explicitly sets, not the browser),*means any page can use a stolen token to read data. Explicit origin restriction doesn't prevent this entirely, but it reduces the attack surface. -
If the endpoint returns different data based on the client's IP address (some internal APIs do this),
*lets any website use the user's IP-based access from their browser.
Stop Blindly Restricting to Specific Origins When Your API Is Public
Here's the practical problem with over-restriction:
// Your public API in production
const allowedOrigins = [
'https://myapp.com',
'https://admin.myapp.com',
'https://staging.myapp.com',
];
Now a third-party developer wants to use your public API from their frontend. They can't. They get a CORS error. They email your support team. Your support team asks your backend team to add their origin. The backend team adds it, deploys, and tells the third-party developer it works now.
Next week, another developer emails. And another. And another.
Meanwhile, any of these developers could have just called your API from their backend and it would have worked immediately, because CORS doesn't apply to server-to-server requests. The origin restriction isn't providing security — it's providing paperwork.
If your API is public and documented and intended for third-party use, use *. Make
it easy for people to use your API. That's the whole point of having a public API.
Conversely, if your API is private and not intended for third-party use, origin restriction alone isn't enough anyway. You need authentication. And if you have authentication, the CORS origin restriction is just an additional layer, not the primary defense.
A Decision Framework: To Wildcard or Not to Wildcard
Walk through these questions in order:
1. Does the endpoint use cookies, HTTP auth, or client certificates?
- Yes → Do NOT use
*. Use explicit origin allowlist withAccess-Control-Allow-Credentials: true. - No → Continue to question 2.
2. Does the endpoint return different data based on who's asking?
- Yes (e.g., based on IP, network location) → Prefer explicit origins.
- No → Continue to question 3.
3. Is the data meant to be public?
- Yes → Use
*. Seriously, just use it. - No → Use explicit origin allowlist.
4. Is the API intended for third-party frontend use?
- Yes → Use
*(assuming no ambient auth from questions 1-2). - No → Use explicit origins for your known frontends.
5. Would restricting the origin provide any actual security benefit?
- Yes → Use explicit origins.
- No → Use
*and save yourself the maintenance burden.
# Quick verification: is * appropriate for this endpoint?
# Test 1: Does the response change with cookies?
curl https://api.example.com/endpoint
curl -H "Cookie: session=test" https://api.example.com/endpoint
# If both responses are identical, cookies don't matter.
# Test 2: Does the endpoint require auth?
curl https://api.example.com/endpoint
# If you get a 401, it requires auth.
# If you get actual data, it's public.
# Test 3: Does the response change based on origin?
curl -H "Origin: https://a.com" https://api.example.com/endpoint
curl -H "Origin: https://b.com" https://api.example.com/endpoint
# If both return the same data, origin doesn't matter.
Closing Thoughts: CORS Is Your Friend, Not Your Enemy
After twenty-two chapters, I want to leave you with a perspective shift.
CORS is not a punishment. It's not an obstacle. It's not a bug in the browser. It is a controlled relaxation of the Same-Origin Policy — a mechanism that lets you open up access to your resources in a precise, granular way.
Before CORS existed, you had two options:
- Same-origin only — no cross-origin access at all
- JSONP — terrifying, hacky, insecure cross-origin access with zero controls
CORS gave us a middle ground: cross-origin access with explicit server consent, method and header restrictions, credential controls, and caching. It's one of the most well-designed security specifications in the web platform.
The fact that CORS is confusing is not because it's badly designed. It's because the problem it solves — allowing controlled cross-origin access in a platform where billions of users visit untrusted websites that run untrusted JavaScript — is inherently complex. The spec makes tradeoffs, and those tradeoffs have corner cases, and those corner cases produce confusing error messages.
But now you understand it. You know what the Same-Origin Policy protects. You know
why preflights exist. You know the difference between simple and non-simple requests.
You know every CORS header and what it does. You can read CORS errors like a human.
You know the common mistakes and how to avoid them. You understand the security
implications. And you know when * is fine.
The next time a coworker comes to you with a CORS error, you won't panic. You'll open the Network tab, find the preflight, check the response headers, and know exactly what to fix. You might even explain it to them.
And maybe, just maybe, you'll pass them this book so they can figure it out next time themselves. I'd like that. I'm tired of fixing everyone's CORS issues.
But I'll always be patient enough to explain.