Back to scan results
Check 21 of 24

CORS Misconfiguration

We send a request with a hostile Origin header and look at how your server responds. A correctly configured server ignores it; a misconfigured one reflects it back, opening the door for any malicious page to read authenticated responses cross-origin.

What this check probes

Three test requests, each with a different Origin header:

  1. Origin: https://attacker.example — a clearly hostile origin. We look at Access-Control-Allow-Origin in the response.
  2. Origin: https://yourdomain.attacker.example — tests for naive substring matching.
  3. Origin: null — sandboxed iframes and some local-file contexts send this; many CORS implementations naively allow it.

For each, we flag a finding when:

  • Access-Control-Allow-Origin reflects the value we sent (any-origin acceptance).
  • Access-Control-Allow-Origin: * is set and Access-Control-Allow-Credentials: true is also set (browsers reject this combo, but if you've coded around it the server is misconfigured anyway).
  • Access-Control-Allow-Origin: null is returned (almost always wrong).
  • A wildcard subdomain pattern matches a hostile lookalike (e.g., *.example.com incorrectly accepts evil.example.com.attacker.com).

Why this matters for PCI DSS

The same-origin policy is what stops evil.com from reading data fetched from your-bank.com while you're logged into both. CORS is the documented hole in same-origin — when your server returns the right headers, browsers are willing to make the cross-origin response readable.

If your server reflects any origin and sends Access-Control-Allow-Credentials: true, then any malicious page a logged-in user visits can:

fetch('https://your-bank.com/account/transactions', {credentials: 'include'})
    .then(r => r.json()).then(data => sendToAttacker(data));

...and exfiltrate the user's authenticated data.

PCI DSS 4.0 Requirement 6.2.4 covers cross-site request forgery and information disclosure — both fall out of broken CORS.

How to fix it

Default to no CORS headers at all. If your API is only called by your own first-party frontend (same origin), you don't need CORS — leave the headers out and the browser will enforce same-origin.

If you genuinely need cross-origin access, maintain an explicit allow-list of trusted origins:

Express (Node.js):

const cors = require('cors');
const allowed = new Set([
    'https://app.example.com',
    'https://admin.example.com',
]);
app.use(cors({
    origin: (origin, cb) => {
        if (!origin || allowed.has(origin)) cb(null, origin);
        else cb(new Error('CORS blocked'));
    },
    credentials: true,
}));

nginx:

map $http_origin $cors_origin {
    "https://app.example.com" $http_origin;
    "https://admin.example.com" $http_origin;
    default "";
}
server {
    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Vary "Origin" always;
}

ASP.NET Core:

builder.Services.AddCors(o => o.AddPolicy("Default", b => b
    .WithOrigins("https://app.example.com", "https://admin.example.com")
    .AllowCredentials()
    .AllowAnyHeader()
    .AllowAnyMethod()));

Rules to live by:

  • Never use a wildcard * on an endpoint that returns sensitive data.
  • Never reflect Origin without first matching it against an exact-match allow-list.
  • Never use substring or regex prefix matching that allows attacker-controlled subdomains.
  • Never accept null as a valid origin.
  • Always include Vary: Origin when the response varies based on origin (so caches don't serve the wrong response).

Test with CORScanner or the curl -H "Origin: ..." command.

Fixed it? Re-run the scan to confirm.

Run scan again