Skip to content

/errors/cors/wildcard-origin-with-credentials

CORS error: 'Access-Control-Allow-Origin' must not be the wildcard '*' when credentials mode is 'include'

The commands on this page have not been executed by our verification runner yet.updated
tool
CORS
versions
Fetch Standard, Chrome, Edge, Firefox, Safari
os
any

The browser blocks a response with Access-Control-Allow-Origin: * when the request was sent with credentials (cookies, HTTP authentication or TLS client certificates). The Fetch standard forbids the wildcard for credentialed requests, so the server must name the origin exactly (Access-Control-Allow-Origin: https://app.example.com) and also send Access-Control-Allow-Credentials: true. Echo the Origin header only for origins on your allow list.

Reproduce it

A page on 127.0.0.1:4810 calls an API on 127.0.0.1:4811 (a different origin, because the port differs) that answers with the wildcard. Microsoft Edge runs headless through playwright-core; the console error is the one users see in DevTools:

bash
npm init -y >/dev/null
npm install --silent --no-audit --no-fund playwright-core@1 >/dev/null 2>&1
cat > cors.js <<'EOF'
const http = require("http");
const { chromium } = require("playwright-core");
const api = http.createServer((req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Content-Type", "application/json");
  res.end('{"ok":true}');
}).listen(4811, "127.0.0.1");
const web = http.createServer((req, res) => res.end("<!doctype html><title>app</title>")).listen(4810, "127.0.0.1");
(async () => {
  const browser = await chromium.launch({ channel: "msedge", headless: true });
  const page = await browser.newPage();
  const errors = [];
  page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
  await page.goto("http://127.0.0.1:4810/");
  const call = (opts) => page.evaluate((o) => fetch("http://127.0.0.1:4811/data", o).then((r) => "HTTP " + r.status, (e) => e.name + ": " + e.message), opts);
  console.log("without credentials:", await call({}));
  console.log("credentials include:", await call({ credentials: "include" }));
  await new Promise((r) => setTimeout(r, 300));
  console.log(errors.filter((e) => e.includes("CORS")).join("\n"));
  await browser.close(); api.close(); web.close();
})();
EOF
node cors.js

The same server works for the request without credentials. JavaScript only sees TypeError: Failed to fetch; the reason is printed in the console, not given to the page.

Fix it on the server

js
// Express with the cors package
const cors = require("cors");
app.use(cors({ origin: ["https://app.example.com", "http://localhost:5173"], credentials: true }));

Without the package, the response to both the preflight and the real request needs:

text
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Vary: Origin stops caches from serving one origin's response to another. With credentials, Access-Control-Allow-Headers and Access-Control-Allow-Methods also lose their * meaning and must list names explicitly.

Sources