Skip to content
StackSiren

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, Authorization header, 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 Origin header back as Access-Control-Allow-Origin for 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:

  1. Access-Control-Allow-Credentials: true
  2. Access-Control-Allow-Origin set to the exact requesting origin — never *
  3. 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

  1. Inventory every response that sets Access-Control-Allow-Origin (app code, middleware, API gateway, CDN).
  2. Classify each endpoint: public/no cookies vs session/cookie vs bearer-token.
  3. Public endpoints may keep * without Access-Control-Allow-Credentials.
  4. Session endpoints: allowlist only your front-end origins; set credentials only if cookies are required.
  5. Add Vary: Origin whenever ACAO is dynamic.
  6. Ship, then run the three curls above from CI or a checklist. Fail the deploy if evil.example receives 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.

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Vercel · Bolt · Cursor

Related posts

Sources