Skip to content
StackSiren

Guide

CORS misconfiguration: wildcard origins, credentials and fixes

When Access-Control-Allow-Origin: * is fine, why reflecting any Origin with credentials is the real risk, and how to set up a safe allowlist.

What CORS does (and doesn't)

CORS is a browser rule that decides whether JavaScript on another website can read your server's responses. It isn't authentication. Tools like curl ignore it completely, so every API still needs its own login and permission checks.

CORS also doesn't stop a request from being sent. It controls whether the other site can read the response, which is why cross-site request forgery needs separate protection.

Is Access-Control-Allow-Origin: * dangerous?

For public data that needs no login, no. Fonts, public APIs and open data are often served this way.

Browsers refuse to share a response that relies on cookies (a credentialed request) when the server answers with Access-Control-Allow-Origin: *. So the wildcard combined with Access-Control-Allow-Credentials: true doesn't work, and it doesn't open your users' sessions to other sites.

The wildcard matters when an API trusts something other than a login, such as being on an internal network. For private APIs, an explicit allowlist is still the safer default.

The real risk: reflecting any Origin with credentials

The dangerous pattern is a server that copies whatever Origin header it receives into Access-Control-Allow-Origin and also sends Access-Control-Allow-Credentials: true. Any website a signed-in user visits can then make requests with that user's cookies and read the answers, such as profile data, API keys or CSRF tokens.

This often happens when CORS errors are fixed by allowing every origin dynamically, for example a CORS library set to accept any origin while credentials are switched on. AI-generated code sometimes does this to make an error disappear.

Other common mistakes

Allowing the null origin. Sandboxed iframes, local files and some redirects send Origin: null, and an attacker can create a null-origin request on purpose. MDN advises against ever returning Access-Control-Allow-Origin: null.

Loose matching. A check that the origin ends with example.com also matches evilexample.com, a check that it starts with https://example.com also matches https://example.com.attacker.net, and an unanchored regular expression can match either.

Trusting too much. Allowing every subdomain, http:// origins or a subdomain with a cross-site scripting bug extends your trust to whoever controls them.

How to check your own API

Send a request with a made-up Origin to an endpoint that returns private data, for example curl -i -H "Origin: https://example.org" followed by your API URL, and read the response headers. If Access-Control-Allow-Origin repeats https://example.org and Access-Control-Allow-Credentials is true, fix it.

Repeat with Origin: null and with a lookalike such as https://yourdomain.com.example.org. For non-GET requests, also send an OPTIONS preflight with an Access-Control-Request-Method header. Only test APIs you own.

How to fix it

Keep a list of exact allowed origins, including scheme, host and port, such as https://app.example.com. Compare the incoming Origin by exact string match. If it matches, echo that origin back. If it doesn't, send no Access-Control-Allow-Origin header at all.

When the allowed origin depends on the request, also send Vary: Origin so caches don't serve one origin's response to another. Only send Access-Control-Allow-Credentials: true if your front end really sends cookies across origins. Never allow null.

If your front end and API share the same origin, you don't need CORS headers at all and can remove them.

Platform notes

Supabase Edge Functions: Supabase's CORS example uses Access-Control-Allow-Origin: *. That works because supabase-js sends the user's token in a request header instead of relying on cookies, and a malicious site doesn't have that token. If a function uses cookies or should only be called from your app, check the Origin against your allowlist instead, echo only allowed origins in Access-Control-Allow-Origin, and keep Access-Control-Allow-Headers as in Supabase's example: authorization, x-client-info, apikey, content-type.

Next.js: set CORS headers in a route handler, including an OPTIONS handler for preflights. Static headers can also go in headers() in next.config, but checking an allowlist needs code, for example in middleware.

Vercel: Vercel Functions don't add CORS headers for you. The headers setting in vercel.json only supports fixed values, though a has condition on the Origin header can limit a rule to one origin. Netlify's _headers file is fixed values too.

What StackSiren checks (and doesn't)

StackSiren makes one normal GET request to the page you submit with a test Origin header. If the response repeats that test origin and also allows credentials, it reports a high-severity finding. If the response has Access-Control-Allow-Origin: *, it reports an informational note, because that is usually fine for public pages.

It doesn't test your API routes, preflight OPTIONS requests, the null origin, lookalike domains or subdomain matching, so check those yourself using the steps above.

Builder-specific checklists

Check your app now

Free passive scan, results in under a minute.

Run a free scan