Restrict CORS to an Origin Allowlist
Replace wildcard CORS with a strict origin allowlist that also handles credentials and preflight safely.
Task
Tighten CORS from wildcard to an explicit allowlist with credentials support.
When to use this
An API reflects any origin, letting malicious sites read authenticated responses in a user’s browser.
Prerequisites
- Access to the API or gateway CORS config
- The list of legitimate front-end origins
- A browser to test preflight
Steps
1. Allow only listed origins and echo the matching one.
const allowed = new Set(['https://app.example.com', 'https://admin.example.com']);
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');
res.set('Access-Control-Allow-Credentials', 'true');
}
next();
});Expected output: Only allowlisted origins receive CORS headers, and responses vary on Origin.
2. Test preflight from an allowed and a disallowed origin.
curl -s -o /dev/null -w '%{http_code}\n' -X OPTIONS https://api.example.com/data \
-H 'Origin: https://evil.example' -H 'Access-Control-Request-Method: GET'Expected output: The disallowed origin receives no CORS headers, blocking the browser read.
Troubleshooting
Problem
Credentials plus wildcard rejected
Solution
Never combine Access-Control-Allow-Credentials with a wildcard origin; echo the exact origin.
Problem
Missing Vary caches wrong origin
Solution
Always send Vary: Origin so shared caches do not reuse a response across origins.
Limitations
- CORS is not an authorization control by itself.
- Does not protect server-to-server clients.
- Requires maintaining the origin list.
What this recipe does
Tighten CORS from wildcard to an explicit allowlist with credentials support.
An API reflects any origin, letting malicious sites read authenticated responses in a user’s browser.
Prerequisites
- Access to the API or gateway CORS config
- The list of legitimate front-end origins
- A browser to test preflight
Procedure
- Allow only listed origins and echo the matching one.
const allowed = new Set(['https://app.example.com', 'https://admin.example.com']);
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');
res.set('Access-Control-Allow-Credentials', 'true');
}
next();
});
Expected result: Only allowlisted origins receive CORS headers, and responses vary on Origin.
- Test preflight from an allowed and a disallowed origin.
curl -s -o /dev/null -w '%{http_code}\n' -X OPTIONS https://api.example.com/data \
-H 'Origin: https://evil.example' -H 'Access-Control-Request-Method: GET'
Expected result: The disallowed origin receives no CORS headers, blocking the browser read.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows allowlisted origins work with credentials while disallowed origins receive no CORS headers.
Troubleshooting
Credentials plus wildcard rejected — Never combine Access-Control-Allow-Credentials with a wildcard origin; echo the exact origin.
Missing Vary caches wrong origin — Always send Vary: Origin so shared caches do not reuse a response across origins.
Limitations
- CORS is not an authorization control by itself.
- Does not protect server-to-server clients.
- Requires maintaining the origin list.