Skip to content

/commands/cors/allow-multiple-origins

Allow multiple origins in Access-Control-Allow-Origin

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
tool
CORS
versions
Fetch Standard, Node.js 18+
os
linux, macos, windows

Access-Control-Allow-Origin accepts a single origin or *, never a list. To allow several origins, keep an allowlist on the server, compare the request's Origin header with it, and when it matches send that one origin back in Access-Control-Allow-Origin, together with Vary: Origin so caches keep a separate copy per origin. Origins that are not on the list get no Access-Control-Allow-Origin header, and the browser blocks the response.

Reflect the origin from an allowlist (Node.js)

bash
cat > api.js <<'EOF'
const http = require("http");
const allowed = new Set(["https://app.example.com", "https://admin.example.com"]);
http.createServer((req, res) => {
  res.setHeader("Vary", "Origin");
  const origin = req.headers.origin;
  if (origin && allowed.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
  }
  if (req.method === "OPTIONS") {
    res.setHeader("Access-Control-Allow-Methods", "GET, POST");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
    res.writeHead(204);
    return res.end();
  }
  res.setHeader("Content-Type", "application/json");
  res.end('{"ok":true}');
}).listen(5208, "127.0.0.1");
setTimeout(() => process.exit(0), 10000);
EOF
node api.js &
sleep 1
for o in https://app.example.com https://admin.example.com https://evil.example; do
  echo "== Origin: $o"
  curl -s -i http://127.0.0.1:5208/items -H "Origin: $o" | tr -d '\r' | grep -iE '^(HTTP|access-control|vary)'
done
wait

Output

== Origin: https://app.example.com
HTTP/1.1 200 OK
Vary: Origin
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
== Origin: https://admin.example.com
HTTP/1.1 200 OK
Vary: Origin
Access-Control-Allow-Origin: https://admin.example.com
Access-Control-Allow-Credentials: true
== Origin: https://evil.example
HTTP/1.1 200 OK
Vary: Origin

Each allowed origin gets its own value back; https://evil.example gets a 200 without the header, which is enough for the browser to refuse it. Vary: Origin is sent on every response, including the refused one, so a shared cache never mixes them up.

The same in nginx

nginx
map $http_origin $cors_origin {
    default                     "";
    "https://app.example.com"   $http_origin;
    "https://admin.example.com" $http_origin;
}
server {
    location /api/ {
        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Vary Origin always;
        proxy_pass http://127.0.0.1:3000;
    }
}

map sets $cors_origin to the request origin only for listed values. nginx does not send a header whose value is empty, so other origins get no header. always adds the header to error responses (4xx, 5xx) too. Use either nginx or the application, not both, or the header appears twice.

Ways to match origins

ApproachWhen to use itWatch out for
Exact allowlist (Set, array, map)A known list of front endsScheme and port are part of the origin: http://localhost:3000 differs from http://localhost:5173
Regular expressionMany subdomains of one domainAnchor both ends, as in ^https://([a-z0-9-]+\.)?example\.com$; an unanchored example\.com also matches evil-example.com
Access-Control-Allow-Origin: *Public data, no cookies or authNot allowed with credentials
Reflect any OriginNever with credentialsEvery site can read authenticated responses

Pitfalls

  • Comma-separated values or two Access-Control-Allow-Origin headers are rejected by the browser with "The 'Access-Control-Allow-Origin' header contains multiple values".
  • The Origin header has no trailing slash or path: compare against https://app.example.com, not https://app.example.com/.
  • Credentialed requests need an explicit origin, never *; see wildcard origin with credentials.
  • Verify each origin from the command line as in test CORS with curl.

FAQ

Can I put several origins in Access-Control-Allow-Origin separated by commas?

No. The header takes exactly one origin, the literal *, or null. A list such as https://a.example, https://b.example never matches, and the browser reports that the header contains multiple values.

Why do I need Vary: Origin?

The response now depends on the Origin request header. Without Vary: Origin, a browser cache or CDN can store the response made for one origin and serve it to another, which then fails CORS.

Is reflecting the Origin header back always safe?

Only when you compare it with an allowlist first. Echoing any Origin together with Access-Control-Allow-Credentials: true lets every website read authenticated responses from your API.

Sources