CORS configuration
The cors package’s documented default configuration is:
{ "origin": "*", "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", "preflightContinue": false, "optionsSuccessStatus": 204}So app.use(cors()) — the line in every getting-started guide — sends
Access-Control-Allow-Origin: * to everyone. That is a deliberate default for a
package used mostly on public APIs, and it is the wrong one for anything behind a
login.
import cors from "cors";
const allowed = new Set(["https://app.example.com"]);
app.use( cors({ origin(origin, cb) { // no origin = same-origin or a non-browser client if (!origin || allowed.has(origin)) return cb(null, origin ?? true); cb(new Error("Not allowed by CORS")); }, credentials: true, }));curl -sI -H 'Origin: https://evil.example' https://api.example.com/me \ | grep -i 'access-control-allow-'No Access-Control-Allow-Origin in that response is the correct result. If it comes
back echoing https://evil.example, the origin is being reflected.
origin: true is the trap, not origin: "*"
Section titled “origin: true is the trap, not origin: "*"”From the package docs: setting origin to true will “reflect the request origin”.
That reads like “enable CORS” and behaves like “allow everyone” — with one crucial
difference from "*". The wildcard is rejected by browsers when credentials are
involved, so origin: "*" plus cookies fails safe. Reflection does not: echoing the
caller’s origin back is a valid, specific value, so origin: true combined with
credentials: true genuinely allows any website to make authenticated requests to
your API and read the responses, using your users’ cookies.
If you take one thing from this page: origin: true with credentials: true is the
combination to grep for.
Regex origins deserve the same suspicion. The docs’ own example, /example\.com$/,
matches notexample.com — there is no boundary before example.
CORS is not access control
Section titled “CORS is not access control”The package’s documentation includes a “Common Misconceptions” section, and it is worth quoting because it contradicts how CORS is usually described:
“CORS protects my API from unauthorized access” — No. CORS is not access control. Any HTTP client (curl, Postman, another server) can call your API regardless of CORS settings.
“CORS blocks requests from disallowed origins” — No. Your server receives and processes every request. CORS headers tell the browser whether JavaScript can read the response.
This matters operationally. A restrictive CORS policy does not stop a request from executing — the side effects still happen, and only the response is withheld from the calling page. Locking down CORS is not a substitute for authentication, and it is not a mitigation for an endpoint that should not have been public.
Tightening origin breaks any front end on a hostname you forgot: preview
deployments, a staging domain, a mobile web wrapper, a partner embed. These fail in
the browser console rather than in your logs, so nobody tells you.
Enumerate the hostnames before deploying, and prefer an explicit allow-list over a regex — the regex is where the subtle holes live.
Related
Section titled “Related”- Helmet and security headers — the other response-header layer
- Rate limiting — because CORS will not slow anyone down