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 Authorization header
  • 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:

  1. Developer sets Access-Control-Allow-Origin: *
  2. Frontend sends credentials: 'include'
  3. Browser refuses (can't use * with credentials)
  4. 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:

  1. Validate the origin against an allowlist
  2. Echo back the validated origin
  3. Set Access-Control-Allow-Credentials: true
  4. 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:

  1. Internal APIs should not be accessible from arbitrary web pages
  2. 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
  3. 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:

  1. If the endpoint returns different data based on the Authorization header (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.

  2. 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 with Access-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:

  1. Same-origin only — no cross-origin access at all
  2. 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.