Wildcard vs Dynamic Origin Reflection: When to Use Each
Two server responses grant cross-origin access: a static Access-Control-Allow-Origin: *, and a dynamically reflected origin echoed back after validation. Choosing the wrong one either breaks credentialed requests or opens the API to every website on the internet. The decision is not a matter of taste — it follows directly from whether the endpoint handles credentials or caller-scoped data.
This page is part of CORS Security Auditing & Hardening, which covers the full process of finding and closing CORS misconfigurations.
The Decision in One Table
| Scenario | Correct choice | Why |
|---|---|---|
| Public fonts, CDN assets, open read-only data, no credentials | Access-Control-Allow-Origin: * |
Response is identical for all origins; simplest and most cacheable |
Authenticated API using cookies or Authorization |
Reflect the validated origin + Allow-Credentials: true |
Wildcard is forbidden with credentials; only listed origins may read |
| Response contains per-tenant or per-user data (even without auth) | Reflect the validated origin | Limits which origins can read caller-scoped information |
| Multiple known front-end origins (app, admin, staging) | Reflect the validated origin from an allowlist | One wildcard cannot distinguish trusted from untrusted callers |
| Unknown / arbitrary origins with credentials | Neither — reject | Reflecting unvalidated origins is a credential-exfiltration vector |
Root Cause: Why the Choice Is Forced
The WHATWG Fetch Standard (§3.2.3) makes one combination illegal: Access-Control-Allow-Origin: * cannot appear with Access-Control-Allow-Credentials: true. The browser checks this at response evaluation and blocks the read. So any endpoint that needs cookies or tokens cannot use a wildcard — it must echo the exact origin. Conversely, an endpoint that is genuinely public and credential-free gains nothing from reflection and is simpler and more cache-friendly with a wildcard, because the response body and headers are identical for every caller. The mechanics of this prohibition are detailed in Understanding Access-Control-Allow-Credentials.
Prerequisite State
- You know whether the endpoint sends credentials (
fetch(..., { credentials: 'include' }), cookies, orAuthorization). - You know whether the response body is public or scoped to the caller.
- You have a defined list of trusted front-end origins if reflection is required.
Step-by-Step: Pick and Implement
Step 1 — Classify the endpoint
Ask two questions: does it carry credentials, and does it return caller-specific data? A “yes” to either rules out the wildcard.
Step 2a — Public + credential-free → wildcard
location /public/ {
add_header Access-Control-Allow-Origin "*" always;
# No Allow-Credentials, no Vary: Origin needed — response is identical for all
}
Step 2b — Credentialed or scoped → validated reflection
Match the origin against an allowlist and reflect only the matched value, adding Vary: Origin so caches key per origin:
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.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin');
}
next();
});
For the allowlist architecture and subdomain-safe matching, see Dynamic Origin Validation Patterns.
Verification
# Public endpoint: wildcard is fine
curl -si https://api.example.com/public/data -H "Origin: https://anything.example" | grep -i access-control-allow-origin
# → access-control-allow-origin: *
# Credentialed endpoint: exact reflection, and deny for unknown origins
curl -si https://api.example.com/v1/me -H "Origin: https://app.example.com" | grep -i access-control
# → access-control-allow-origin: https://app.example.com + access-control-allow-credentials: true
curl -si https://api.example.com/v1/me -H "Origin: https://attacker.example" | grep -i access-control-allow-origin
# → (no header — correctly denied)
Security Boundary Note
Never reflect the Origin header without validating it against an allowlist. Unvalidated reflection behaves like a wildcard that also works with credentials, letting any website read authenticated responses — a direct data-exfiltration and CSRF-amplification vector. If you find yourself reaching for Access-Control-Allow-Origin: * on an endpoint that uses credentials, the answer is not a wildcard and not blind reflection; it is a validated allowlist. Deeper threat modelling is in Wildcard Risks & Mitigation.
Common Mistakes
| Issue | Technical impact | Mitigation |
|---|---|---|
| Wildcard on a cookie-authenticated endpoint | Browser blocks the credentialed response; the API appears broken | Reflect the validated origin with Allow-Credentials: true |
Reflecting Origin without an allowlist |
Any site can read authenticated responses | Match against a trusted set before echoing |
Reflection without Vary: Origin |
CDN serves one origin’s header to another; intermittent failures | Add Vary: Origin to every reflected response |
| Wildcard on a per-tenant public endpoint | Any origin can read another tenant’s data | Reflect a validated origin even without credentials |
FAQ
Is Access-Control-Allow-Origin: * ever safe?
Yes, for genuinely public resources that carry no credentials and expose no per-user or per-tenant data: public fonts, open datasets, CDN assets, and unauthenticated read-only APIs. There the wildcard is simpler and cache-friendly because the response is identical for every origin. It becomes unsafe the moment the endpoint relies on cookies or tokens or returns caller-scoped data.
Why can’t I use a wildcard with credentials?
The WHATWG Fetch Standard forbids Access-Control-Allow-Origin: * when Access-Control-Allow-Credentials is true. The browser rejects the response before JavaScript can read it, preventing any origin from reading an authenticated response. With credentials you must echo the exact validated origin string.
Is reflecting the Origin header the same as a wildcard?
Only if you reflect it without validation. Echoing whatever Origin arrives with no allowlist is effectively a wildcard that also works with credentials — the worst case, since any site can then read authenticated responses. Reflection is safe only when the origin is first matched against a trusted allowlist and the response includes Vary: Origin.