GuidesHEADERS

Allow multiple origins in CORS

The Access-Control-Allow-Origin response header holds exactly one origin, so you cannot put a list of domains in the value. To allow multiple origins in CORS, the server reads the incoming Origin request header and picks the grant per request, checked against an explicit allowlist.

The header holds exactly one value

Access-Control-Allow-Origin accepts a single exact origin, the wildcard *, or null, and nothing else. An origin is scheme plus host plus port: https://app.example.com and http://app.example.com are different origins, and so are two different ports on the same host.

Because one header cannot contain several origins, there is no static configuration that grants a list of sites. The syntax and matching rules are covered in the Access-Control-Allow-Origin guide.

Why a comma-separated list fails

A comma-separated list in Access-Control-Allow-Origin does not do what it suggests. The browser does not split the value on commas; it reads the entire string as one origin, which is invalid and matches nothing.

Everything except the per-request echo fails
# Invalid: the browser reads this as one origin string and nothing matches
Access-Control-Allow-Origin: https://app.example.com, https://admin.example.com

# Invalid: wildcard subdomains are not header syntax
Access-Control-Allow-Origin: https://*.example.com

# The only valid multi-origin answer: pick the right single origin per request
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

Since the request origin never equals the combined string, the origin check fails, the browser blocks the response, and the console logs a CORS error even though the server "allowed" both sites.

Whitelist and echo the origin

The standard pattern inspects each request and echoes the caller origin only when it appears on an internal allowlist. Origins outside the list receive no grant.

  1. Read the Origin header

    Inspect the Origin header the browser attaches to cross-origin requests.

  2. Compare against the allowlist

    Check the exact serialized origin string against a server-side set of approved origins.

  3. Echo the match and add Vary

    Set Access-Control-Allow-Origin to the matched origin and append Vary: Origin to the response.

Allowlist and echo in a Node-style handler
const allowedOrigins = new Set([
  'https://app.example.com',
  'https://admin.example.com',
]);

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && allowedOrigins.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
  }
  next();
});

Vary: Origin is required whenever the echoed value changes per request. Without it, shared caches can serve a response stored for one origin to a client from another origin, causing intermittent CORS failures that are painful to reproduce.

Regex and matching pitfalls

Matching origins with loose regular expressions is where multi-origin setups go wrong. An insecure pattern lets attacker domains that merely resemble your hostnames pass validation.

  • An unescaped dot matches any character: the pattern example.com also accepts exampleXcom.
  • Missing end anchors let patterns like example.com.* match attacker domains such as example.com.evil.com.
  • Scheme-less patterns can match plain http origins when only https was intended.
  • Reflecting any incoming Origin blindly turns the API into an open relay for credentialed cross-site reads; echo only origins from a reviewed allowlist.
Match the parsed origin, not a loose pattern
// Bad: matches example.com.evil.com, and the dots match any character
/^https?:\/\/example.com.*/

// Good: parse first, then compare with a dot boundary
function isAllowed(origin: string) {
  let url: URL;
  try {
    url = new URL(origin);
  } catch {
    return false;
  }
  if (url.protocol !== 'https:') return false;
  return url.host === 'example.com' || url.host.endsWith('.example.com');
}

Allowing wildcard subdomains

A value like https://*.example.com is not valid header syntax and never works in Access-Control-Allow-Origin. Subdomain support is server-side matching logic, not header syntax.

  • Parse the incoming Origin into a URL instead of running substring checks.
  • Require the https scheme, then check the apex domain as an exact match and subdomains with an anchored dot boundary such as endsWith(".example.com").
  • Echo the exact incoming origin string once validation passes, with Vary: Origin on the response.

When a different architecture is cleaner

Dynamic allowlists spread across many services get fragile. When origins keep multiplying, changing the topology can remove the problem instead of managing it.

  • Serve every frontend from one API hostname so requests stay same-origin and CORS disappears.
  • Centralize the allowlist in an API gateway or reverse proxy so one layer owns CORS for every service behind it.
  • For third-party APIs you do not control, call them from your own backend, or use a public proxy like cors.dev for public, non-credentialed data. See proxy versus own backend for the trade-offs.

Good questions.

Can I use a comma-separated list of origins in Access-Control-Allow-Origin?

No. The header accepts one origin, the wildcard *, or null. A comma-separated list is read as a single malformed origin that matches nothing, so the browser blocks the response.

Can I put https://*.example.com in the header?

No. Wildcard subdomains are not valid header syntax. Parse the Origin header on the server, validate the subdomain with matching logic, and echo the exact origin.

Why is reflecting the Origin header blindly dangerous?

Echoing any incoming Origin without an allowlist turns the API into an open relay for credentialed cross-site reads. Only origins from a reviewed allowlist should be echoed.

Why does the echo pattern need Vary: Origin?

Vary: Origin tells caches to store the response per requesting origin. Without it, a cache can serve the grant meant for one origin to a different origin, causing CORS failures.