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.
originoptionWhich origins may call the API. A bare cors() allows every origin. true is the worse trap: it reflects the caller's own origin, and unlike the wildcard — which browsers refuse alongside credentials, so it fails closed — reflection plus credentials genuinely lets any site make authenticated requests.
- accepts
"*"|true (reflects request origin)|<string>|<regex>|<function>- default
"*"- set in
cors() options
methodsoptionMethods allowed for cross-origin requests. The default set is broad and includes every write verb.
- accepts
GET,HEAD,PUT,PATCH,POST,DELETE- default
GET,HEAD,PUT,PATCH,POST,DELETE- set in
cors() options
Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.The browser refused the response because the server sent no CORS header at all. Nothing failed server-side — the request was received and handled, and the rejection happens in the browser after the response arrives, which is why the server log looks clean.
Read in: chromium third_party/blink/renderer/platform/loader/cors/cors_error_string.ccAccess to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.The wildcard and credentials cannot be combined — the browser enforces that. The dangerous fix is switching the wildcard to origin reflection, which satisfies this message while making every origin trusted with credentials; the safe fix is an explicit allow-list.
Read in: chromium third_party/blink/renderer/platform/loader/cors/cors_error_string.ccorigin: 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