Access-Control-Allow-Headers: * looks like a convenient catch-all for CORS header configuration — but its behavior is not the same as Access-Control-Allow-Origin: *, and combining it with Access-Control-Allow-Credentials: true produces a result that many developers find surprising. Understanding exactly what the wildcard does, when browsers block it, and how to configure allowed headers correctly will save you from both security vulnerabilities and hard-to-debug CORS errors.
What Access-Control-Allow-Headers Does
When a browser sends a preflight request (an HTTP OPTIONS request before a cross-origin request that uses custom headers, a non-simple method, or a non-simple content type), the server responds with CORS headers indicating what the actual request is permitted to send.
Access-Control-Allow-Headers tells the browser which request headers are allowed in the actual cross-origin request:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.yourcompany.com
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Max-Age: 86400
In this example, the browser is told it may send Content-Type, Authorization, and X-Requested-With headers in the actual request. Any other custom header (e.g., X-Custom-Token) would be blocked by the browser.
When Access-Control-Allow-Headers: * Is Allowed
The wildcard * in Access-Control-Allow-Headers was formally added in the Fetch specification update and is supported by all modern browsers. When used without credentials, it means “any request header is allowed”:
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *
This is valid and works in modern browsers for unauthenticated, non-credentialed requests — the same scenarios where Access-Control-Allow-Origin: * is acceptable (public APIs, open data, CDN assets).
The Critical Exception: Wildcards Are Blocked with Credentials
The browser specification explicitly forbids wildcard values when Access-Control-Allow-Credentials: true is set. From the Fetch specification:
If
credentialsistrueandheaderis*, return failure.
This means the following combination does not work and will be blocked by all modern browsers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
The browser will reject this response because wildcards and credentials cannot be combined. This is a deliberate security measure: if the browser allowed credentialed requests (which carry session cookies, Authorization headers, and TLS client certificates) to any origin with any header, it would enable widespread cross-site credential theft.
What the Browser Error Looks Like
In Chrome’s console, you’ll see something like:
Access to fetch at 'https://api.yoursite.com/data' from origin 'https://app.yoursite.com'
has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header
in the response is '' which must be 'true' when the request's credentials mode is 'include'.
Or alternatively:
Access to fetch at 'https://api.yoursite.com/data' from origin 'https://app.yoursite.com'
has been blocked by CORS policy: Cannot use wildcard in Access-Control-Allow-Origin when
credentials flag is true.
The exact message varies depending on which wildcard combination is invalid, but the root cause is the same: credentials + wildcard is not permitted.
The Authorization Header Is Not Covered by Wildcard
There is an additional nuance specific to the Authorization header. Even when Access-Control-Allow-Headers: * is used in a non-credentialed context, the Authorization header is explicitly excluded from the wildcard match.
From the Fetch specification:
The
AuthorizationHTTP header cannot be added using*; theAuthorizationheader must be listed explicitly.
This means:
# Does NOT allow Authorization header in cross-origin requests
Access-Control-Allow-Headers: *
# Does allow Authorization header
Access-Control-Allow-Headers: *, Authorization
Or, more explicitly and safely:
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-API-Key
This catches many developers by surprise — they set a wildcard thinking all headers are covered, then find that Authorization: Bearer <token> headers are still being blocked by the browser.
Secure CORS Header Configuration for API Servers
For APIs that require authentication (virtually all production APIs), the correct CORS configuration is:
Node.js / Express
const allowedOrigins = [
'https://app.yourcompany.com',
'https://yourcompany.com',
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
// Explicit origin — required when credentials are involved
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') {
// Explicit header list — wildcard blocked with credentials
res.setHeader('Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, X-API-Key'
);
res.setHeader('Access-Control-Allow-Methods',
'GET, POST, PUT, PATCH, DELETE, OPTIONS'
);
res.setHeader('Access-Control-Max-Age', '86400');
return res.sendStatus(204);
}
next();
});
Python / FastAPI
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.yourcompany.com",
"https://yourcompany.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=[
"Content-Type",
"Authorization",
"X-Requested-With",
"X-API-Key",
],
# Do NOT use allow_headers=["*"] with allow_credentials=True
)
Python / Flask
from flask import Flask, request
from flask_cors import CORS
app = Flask(__name__)
CORS(
app,
origins=["https://app.yourcompany.com", "https://yourcompany.com"],
supports_credentials=True,
allow_headers=[
"Content-Type",
"Authorization",
"X-Requested-With",
],
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
)
Java / Spring Boot
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
// Explicit origins — wildcard not allowed with credentials
config.setAllowedOrigins(List.of(
"https://app.yourcompany.com",
"https://yourcompany.com"
));
// Explicit headers — wildcard blocks Authorization header
config.setAllowedHeaders(List.of(
"Content-Type",
"Authorization",
"X-Requested-With",
"X-API-Key"
));
config.setAllowedMethods(List.of(
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
));
config.setAllowCredentials(true);
// Cache preflight response for 1 hour
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
The Vary: Origin Header Is Required
When you dynamically set Access-Control-Allow-Origin based on the request’s Origin header (the correct approach for credentialed requests), you must also include Vary: Origin in the response:
Access-Control-Allow-Origin: https://app.yourcompany.com
Vary: Origin
Without Vary: Origin, a CDN or reverse proxy cache may store the CORS response for one origin and serve it to requests from a different origin. This causes requests from other legitimate origins to fail, or — worse — causes CORS headers for the wrong origin to be served, creating both a security and reliability issue.
Testing Your CORS Header Configuration
Use curl to test your preflight response:
# Test preflight with a custom header
curl -X OPTIONS https://api.yoursite.com/endpoint \
-H "Origin: https://app.yoursite.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization, Content-Type" \
-v
# Check the response for:
# Access-Control-Allow-Headers: (should include Authorization and Content-Type)
# Access-Control-Allow-Origin: https://app.yoursite.com (exact, not wildcard)
# Access-Control-Allow-Credentials: true
And test from an unexpected origin:
# Should receive NO CORS headers (or an error) — not a wildcard response
curl -X OPTIONS https://api.yoursite.com/endpoint \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: GET" \
-v
A safe server should return no Access-Control-Allow-Origin header for unrecognized origins — not a wildcard.
What SAST Tools Detect
A static application security testing tool analyzing your server-side code should flag:
- Wildcard
Access-Control-Allow-Originon endpoints that handle authentication or return user-specific data - Reflected origin — any code that copies the incoming
Originheader value directly intoAccess-Control-Allow-Originwithout validation - Wildcard
Access-Control-Allow-Headers: *combined withAccess-Control-Allow-Credentials: true— which is both a specification violation and a security concern - Missing
Vary: Originwhen the origin is set dynamically
In Offensive360, this maps to the CORSAllowOriginWildcard and CORSAllowCredentials rules in the Knowledge Base, which detect these patterns across Node.js, Python, Java, PHP, Go, and C# codebases.
Common Misconfiguration Patterns and Their Risks
Pattern 1: Wildcard With Credentials (Specification Violation)
// WRONG — browsers block this combination
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
Risk: Browsers block the request, causing your application to break. If you encounter this in production code, it means credentialed cross-origin requests are failing for all clients.
Pattern 2: Reflected Origin Without Validation (Critical Security Issue)
// WRONG — any origin is reflected back, including malicious sites
const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin); // No validation
res.setHeader('Access-Control-Allow-Credentials', 'true');
Risk: Any website can make credentialed requests to your API and read the responses, enabling cross-site data theft. This is functionally the most dangerous CORS misconfiguration.
Pattern 3: Wildcard Headers with Missing Authorization Header
// INCOMPLETE — wildcard doesn't cover Authorization per spec
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', '*');
// Authorization header is still blocked in cross-origin requests
Risk: JavaScript clients using Authorization: Bearer ... headers will fail for cross-origin requests even though the wildcard appears to allow everything.
Pattern 4: Correct — Explicit Allowlist
// CORRECT — explicit origins and explicit headers
if (allowedOrigins.includes(req.headers.origin)) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With'
);
}
Summary: Access-Control-Allow-Headers Wildcard Rules
| Configuration | Non-Credentialed Requests | Credentialed Requests |
|---|---|---|
Allow-Headers: * (no credentials) | ✅ Allowed | — |
Allow-Headers: * + Allow-Credentials: true | ❌ Blocked by browsers | ❌ Blocked by browsers |
Allow-Headers: * (covers Authorization?) | ❌ No — Authorization excluded | — |
Allow-Headers: Content-Type, Authorization | ✅ Correct | ✅ Correct |
Allow-Origin: * + Allow-Credentials: true | ❌ Blocked by browsers | ❌ Blocked by browsers |
Explicit origin allowlist + Allow-Credentials: true | ✅ Correct | ✅ Correct |
The rule: For any API that uses authentication, use an explicit origin allowlist and an explicit header list. Never use wildcards with Access-Control-Allow-Credentials: true. Always include Vary: Origin when the Access-Control-Allow-Origin value is set dynamically.
Offensive360 SAST detects wildcard CORS configurations, reflected-origin patterns, and credential/wildcard conflicts in your source code. See the full CORS vulnerability reference in the Knowledge Base or run a one-time scan for $500 to check your API configuration.