Configure CORS with an Origin Allowlist
Allow only trusted browser origins, handle preflight requests correctly, and avoid the insecure combination of wildcard origins with credentials.
Task
Configure cross-origin access for a browser API using an explicit allowlist instead of reflecting every origin.
When to use this
A frontend on a different origin calls your API. You need CORS for that frontend without letting arbitrary sites read authenticated responses.
Prerequisites
- A list of trusted origins per environment
- Access to the API middleware chain
- A way to test preflight and credentialed requests
Steps
1. Store allowed origins in configuration, not in code. Use exact scheme, host, and port.
const allowedOrigins = new Set(
(process.env.CORS_ORIGINS ?? '').split(',').map((s) => s.trim()).filter(Boolean),
);
Expected output: The API starts with an explicit origin set; an empty set denies cross-origin access.
2. Echo the request origin only when it is allowed. Add Vary: Origin so caches do not mix responses across origins.
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && allowedOrigins.has(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Vary', 'Origin');
res.set('Access-Control-Allow-Credentials', 'true');
}
next();
});
Expected output: Allowed origins receive the header; unknown origins do not.
3. Handle OPTIONS preflight with the allowed methods and headers, and return 204 without running business logic.
app.options('*', (req, res) => {
const origin = req.headers.origin;
if (origin && allowedOrigins.has(origin)) {
res.set('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE');
res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.set('Access-Control-Max-Age', '600');
}
res.sendStatus(204);
});
Expected output: A valid preflight returns 204 with the allowed methods and headers.
4. Verify with an allowed origin, a disallowed origin, and a credentialed request. Confirm that no origin is reflected blindly.
curl -i -H 'Origin: https://evil.example' http://localhost:3000/api/meExpected output: No Access-Control-Allow-Origin header for the disallowed origin.
Troubleshooting
Problem
Cookies are not sent cross-origin
Solution
Use exact origins (not *) with Access-Control-Allow-Credentials: true, and set SameSite=None; Secure on the cookie.
Problem
Preflight fails for a custom header
Solution
Add the header to Access-Control-Allow-Headers and ensure the preflight route returns 2xx.
Limitations
- CORS is a browser control, not authentication; direct server-to-server requests ignore it.
- Allowing null origins or reflecting any origin defeats the allowlist.
What CORS protects
CORS tells the browser whether a cross-origin page may read a response. It does not stop a server or a command-line client from calling the API. The allowlist must therefore be paired with authentication and authorization.
const allowed = new Set(process.env.CORS_ORIGINS.split(','));
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && allowed.has(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Vary', 'Origin');
}
next();
});
Never combine Access-Control-Allow-Origin: * with credentials; browsers reject it, and reflecting arbitrary origins is equivalent to disabling the allowlist.
Verify
- An allowed origin receives
Access-Control-Allow-Originwith its exact value. - A disallowed origin receives no CORS headers.
- Preflight returns 204 with the required methods and headers.
- Responses include
Vary: Originso a shared cache cannot serve one origin’s headers to another. - Credentialed requests work only with exact origins and correctly configured cookies.
Common mistakes
Reflecting the Origin header without checking it; allowing null; forgetting Vary; and handling preflight through the same expensive middleware as normal requests. Keep preflight cheap and explicit.