← Recipes

阅读中文版本

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.

~20 minsecurityhttpapi-design

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/me

Expected 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-Origin with its exact value.
  • A disallowed origin receives no CORS headers.
  • Preflight returns 204 with the required methods and headers.
  • Responses include Vary: Origin so 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.