Why does CORS fail when Access-Control-Allow-Origin is * with credentials?

Sending both Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true looks permissive, but browsers reject that response and the cross-origin request fails. A CORS wildcard with credentials is invalid per the CORS protocol, and it usually signals a copy-pasted header block that was never meant to be combined. This page explains what the wildcard really permits and how to return the correct headers.

Why this matters

Access-Control-Allow-Origin tells the browser which origins may read a response. The literal value * is a wildcard meaning any origin, but MDN is explicit that using the wildcard together with credentials results in an error — for a credentialed request the header must name a single origin, or the request is blocked. Credentials, as defined in the Fetch Standard, are HTTP cookies, TLS client certificates, and authentication entries for HTTP authentication. The practical result is a broken integration: a front end sends credentials with fetch(url, { credentials: "include" }), the server answers with *, and the browser refuses to expose the response. Crucially, the wildcard does not grant authenticated cross-origin reads — the browser stops that read entirely. The real risk is what frequently sits beside this configuration. OWASP's CSRF Prevention Cheat Sheet notes that the browser will reject any response that includes Access-Control-Allow-Origin: * if credentials are allowed, and warns against reflecting arbitrary origins. The dangerous variant is a server that echoes back whatever Origin it receives and also sets Access-Control-Allow-Credentials: true; that pattern lets any site read authenticated responses, which is a serious data leak. Access-Control-Allow-Credentials only accepts the exact value true, in lowercase, and must be omitted rather than set to false when credentials are not needed.

How to fix it

Stop using the wildcard for any endpoint that needs credentials. Keep an allowlist of origins, compare the incoming Origin header against it, and echo back only the matching origin. Add Vary: Origin so caches do not serve one origin's response to another. Set Access-Control-Allow-Credentials: true only when the endpoint truly needs cookies or client certificates, and never combine it with *. The snippets below use the documented header APIs of each stack.

const allowed = new Set(['https://app.example.com']);

app.use((req, res, next) => {
  const origin = req.get('Origin');
  if (allowed.has(origin)) {
    res.set('Access-Control-Allow-Origin', origin);
    res.set('Access-Control-Allow-Credentials', 'true');
    res.vary('Origin');
  }
  next();
});
const origin = request.headers.get("Origin");
const headers = new Headers();
if (origin === "https://app.example.com") {
  headers.set("Access-Control-Allow-Origin", origin);
  headers.set("Access-Control-Allow-Credentials", "true");
  headers.set("Vary", "Origin");
}
return new Response(body, { headers });

Apache sets a fixed allowed origin with mod_headers and uses the merge action to add Origin to Vary. Cloudflare Pages note that if an endpoint must serve cross-origin requests, the Worker or Function code should attach the CORS headers itself, including on the preflight OPTIONS request.

Header set Access-Control-Allow-Origin "https://app.example.com"
Header set Access-Control-Allow-Credentials "true"
Header merge Vary Origin

How to verify the fix

Send a request with an Origin header and a session cookie, then inspect the response headers. For a credentialed request the Access-Control-Allow-Origin value must equal the origin you sent — not * — or the header should be absent entirely. Check that Access-Control-Allow-Credentials is present only when needed, and that Vary includes Origin.

curl -i -H "Origin: https://evil.example" -H "Cookie: session=abc" https://api.example.com/me

Automated check

App Readiness Auditor sends requests with a test Origin header and inspects the CORS response headers. When it sees Access-Control-Allow-Origin set to * together with Access-Control-Allow-Credentials: true, it reports "CORS misconfiguration: wildcard with credentials" at High severity. It also flags a server that reflects an arbitrary Origin alongside credentials. The scanner reports the headers it actually observed; endpoints that require authentication to reach are marked as not fully checked rather than assumed clean.

Scan your app free →

Related issues