Skip to content

CORS configuration

Severity: highApplies to: cors 2.xFacts last verified 2026-07-26 against cors 2.8.6

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.

the fix
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,
})
);
verify it workedRun this in: http response
Terminal window
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.

settings on this page
originoption

Which 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
Read in: cors 2.8.6 lib/index.js
methodsoption

Methods 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
Read in: cors 2.8.6 lib/index.js
if you see this
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.cc
Access 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.cc

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.

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.

before you ship this

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.