Is Access-Control-Allow-Origin: * safe?
By StackSiren · Published
Access-Control-Allow-Origin: * is fine for public, cookie-free APIs. The real risk is reflecting any Origin while also sending Access-Control-Allow-Credentials: true, which lets a stranger's site read authenticated responses from your API.
CORS is a browser gate on which pages may read a cross-origin response. It does not replace auth, rate limits or RLS. Only test APIs and sites you own.
When * is fine
Browsers treat Access-Control-Allow-Origin: * as "any origin may read this response" — but only for requests without credentials. MDN is explicit: for credentialed requests the server must name a single origin; * is not allowed with credentials.
Safe uses of *:
- Public CDN assets, docs, and marketing APIs that never set cookies for the client
- Responses that do not depend on a session cookie,
Authorizationheader, or TLS client cert from the caller - APIs where every authenticated call already proves identity with a bearer token you validate server-side, and you are not also sending
Access-Control-Allow-Credentials: true
Unsafe patterns:
- Echoing the request's
Originheader back asAccess-Control-Allow-Originfor every caller - Combining that reflection with
Access-Control-Allow-Credentials: true - Assuming CORS alone stops a scripted attacker — it only constrains browsers
Full background: CORS misconfiguration. Headers that sit alongside CORS are covered in add security headers on Vercel and Netlify security headers.
Credentials + reflected origin
A credentialed cross-origin request is one the browser sends with cookies or HTTP auth (for fetch, credentials: "include"; for XHR, withCredentials = true). For the response to be readable, MDN requires:
Access-Control-Allow-Credentials: trueAccess-Control-Allow-Originset to the exact requesting origin — never*- No
*wildcards on allow-headers / allow-methods / expose-headers either
The dangerous middleware looks like this:
// UNSAFE — reflects any Origin and allows cookies
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Vary", "Origin");
}
next();
});
Any site the victim visits can then call your API with the victim's cookies and read the JSON. That is the classic CORS credential misconfiguration, not the mere presence of *.
Also note: if you answer a credentialed request with Access-Control-Allow-Origin: *, the browser blocks the response. So some apps "fix" CORS by switching to reflection — and accidentally open the door wider.
Allowlist example
Name the origins you trust. Reject everything else. Include Vary: Origin when the ACAO value depends on the request.
// Express / Node — allowlist only
const ALLOWED = new Set([
"https://yourapp.com",
"https://www.yourapp.com",
"https://staging.yourapp.com",
]);
function corsAllowlist(req, res, next) {
const origin = req.headers.origin;
if (origin && ALLOWED.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Vary", "Origin");
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
}
if (req.method === "OPTIONS") {
return res.status(204).end();
}
next();
}
app.use(corsAllowlist);
Next.js Route Handler sketch (same idea):
// app/api/data/route.ts
const ALLOWED = new Set(["https://yourapp.com", "https://www.yourapp.com"]);
export async function OPTIONS(req: Request) {
const origin = req.headers.get("origin") ?? "";
if (!ALLOWED.has(origin)) return new Response(null, { status: 403 });
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
Vary: "Origin",
},
});
}
Prefer bearer tokens over cookies for cross-origin SPAs when you can. Then you can often keep cookies out of the picture and use a tighter CORS policy. If you do use cookies, set SameSite appropriately and treat CORS as one layer, not the only one.
For apps scaffolded in Cursor or Bolt and hosted on Vercel, set these headers in your API code (Route Handlers, serverless functions), not only in vercel.json page headers — page headers do not replace API CORS. See the Cursor app security checklist.
curl check
Only against APIs you own. Replace hosts and paths.
1. Public / wildcard case (no credentials expected)
curl -sI -H "Origin: https://evil.example" \
https://api.yourapp.com/public/health \
| grep -iE 'access-control-allow-|vary'
Expect either no ACAO (browser will block cross-origin reads — fine for same-origin-only APIs) or Access-Control-Allow-Origin: * / your known public origin, and no Access-Control-Allow-Credentials: true.
2. Credentialed / session API
curl -sI -H "Origin: https://evil.example" \
https://api.yourapp.com/me \
| grep -iE 'access-control-allow-|vary'
Pass: no ACAO for evil.example, or a 403/empty CORS pair. Your real front-end origin should get an exact ACAO match plus credentials only if you truly need cookies.
Fail: Access-Control-Allow-Origin: https://evil.example together with Access-Control-Allow-Credentials: true.
3. Preflight
curl -sI -X OPTIONS \
-H "Origin: https://yourapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type,authorization" \
https://api.yourapp.com/me \
| grep -iE 'access-control-allow-|vary'
Confirm methods and headers match what the browser will send. Our security headers checker reports response headers on URLs you own; use it alongside these curls when you are auditing a deploy.
Common framework mistakes
Express cors package with origin: true. That option reflects the request Origin. Combined with credentials: true it is the same unsafe pattern as hand-rolled reflection. Prefer origin: ["https://yourapp.com", …] or a function that returns only allowlisted origins.
Next.js middleware that copies Origin blindly. Middleware runs on many paths. Scope CORS headers to API routes you intend to expose cross-origin, and keep page documents on a same-origin policy unless you truly need otherwise.
Supabase Edge Functions / serverless “quick CORS” snippets. Tutorials often paste Access-Control-Allow-Origin: * for demos. Fine for a public, cookie-free function. Wrong if the function reads a session cookie or returns per-user data and you later flip on credentials without an allowlist.
CDN or host defaults. Some proxies add ACAO for “helpful” debugging. After every infra change, re-run the evil-origin curl against the production hostname — not only localhost.
Confusing CORS with CSRF. CORS stops a browser script on evil.example from reading your response. It does not stop the browser from sending a simple cookie-authenticated POST in some cases. Use CSRF tokens or SameSite cookies for state-changing cookie auth; use CORS as a separate control for readable APIs.
Fix checklist
- Inventory every response that sets
Access-Control-Allow-Origin(app code, middleware, API gateway, CDN). - Classify each endpoint: public/no cookies vs session/cookie vs bearer-token.
- Public endpoints may keep
*withoutAccess-Control-Allow-Credentials. - Session endpoints: allowlist only your front-end origins; set credentials only if cookies are required.
- Add
Vary: Originwhenever ACAO is dynamic. - Ship, then run the three curls above from CI or a checklist. Fail the deploy if
evil.examplereceives credentials-enabled ACAO.
StackSiren's free first scan of a site you own flags noisy header issues; pair it with the targeted curls for APIs.
FAQ
Is Access-Control-Allow-Origin: * always a vulnerability?
No. For public, non-credentialed responses it is a normal, documented pattern. Treat reflection + credentials as the high-severity case.
Why did my browser reject * when I sent cookies?
Because the CORS protocol forbids * with credentialed requests. The browser blocks the response rather than exposing cookie-backed data to a random origin.
Can I list multiple origins in one ACAO header?
No. Send exactly one origin (or * without credentials). Maintain an allowlist in code and echo only a match.
Does CORS stop Postman, curl or a server-side scraper? No. Those tools are not browsers. Protect data with auth, RLS and rate limits regardless of CORS.
Should every API use Access-Control-Allow-Credentials: true?
Only if the browser must send cookies or HTTP auth cross-origin. Prefer tokens in Authorization when you can, and keep credentials off public endpoints.
Where do I set this on Vercel / Netlify?
In the code that answers the API request (Route Handlers, serverless / edge functions, Express). Static _headers / vercel.json page headers are for documents, not a substitute for careful API CORS.
How do I keep this from regressing?
Add the evil-origin curl to CI after each deploy, or schedule a scan. StackSiren's first scan of a site you own is free.
Fix guides for this topic
Platform checklists: Vercel · Bolt · Cursor