[GUIDE]

CORS Misconfiguration: How to Test and Fix a Permissive CORS Policy

A CORS misconfiguration is a server-side cross-origin policy that grants access too broadly — most dangerously, one that reflects whatever Origin header the caller sends back in Access-Control-Allow-Origin while also setting Access-Control-Allow-Credentials: true. That combination lets any website make authenticated requests to your API and read the responses. To test for it, send a request with a forged Origin header (for example curl -H 'Origin: https://evil.com' -I https://api.yoursite.com) and check whether the server reflects evil.com back. A correctly configured server ignores origins it does not trust.

This guide explains what CORS is, why reflecting the Origin header is the core mistake, how to run a CORS checker against your own API from the command line, and how to fix a permissive policy.

What is CORS, and what is a CORS misconfiguration?

Cross-Origin Resource Sharing (CORS) is a browser mechanism that relaxes the same-origin policy — the default rule that JavaScript on site-a.com cannot read responses from site-b.com. When a server wants to allow a specific other origin to read its responses, it opts in with response headers like Access-Control-Allow-Origin. The browser enforces these rules; the server merely declares them. The canonical reference is MDN's CORS documentation.

A CORS misconfiguration is when those declarations are too loose, so origins that should not be trusted are. The classic failure is treating the Origin request header as trustworthy input. The Origin header is set by the browser but is fully attacker-controlled from any other context — a script on a malicious page, or a curl command, can claim any origin it likes. If your server reads that value and echoes it straight back into Access-Control-Allow-Origin, you have effectively allowed every origin. This class of weakness is catalogued as CWE-942: Permissive Cross-domain Policy with Untrusted Domains.

Why is reflecting the Origin header dangerous?

The danger is concentrated in one combination: reflecting an arbitrary origin and allowing credentials.

Browsers refuse to send cookies or HTTP auth on a cross-origin request unless the response sets Access-Control-Allow-Credentials: true, and they refuse to combine that flag with a literal Access-Control-Allow-Origin: *. So a developer who wants credentialed cross-origin requests to "just work" sometimes reflects the request's Origin back verbatim instead of a wildcard. That dodges the browser's guard while producing the same any-origin access — the worst of both worlds.

When this is in place, an attacker hosts a page, a victim who is logged into your app visits it, and the attacker's JavaScript issues a fetch(..., { credentials: 'include' }) to your API. The victim's session cookie rides along, your server reflects the attacker's origin with credentials: true, and the browser hands the authenticated response body to the attacker's script. They can now read the victim's private data. OWASP covers this attack pattern in its guidance on CORS Origin header scrutiny, and PortSwigger's CORS reference walks through the exploit step by step.

Which CORS patterns are dangerous?

Not every permissive header is a vulnerability — context matters. The table below lists the patterns a CORS checker looks for, why each is risky, and the fix.

Response patternWhy it is dangerousFix
Access-Control-Allow-Origin: * with private/authenticated dataAny site can read the response. (Browsers block * + credentials, but data exposed without auth still leaks.)Use * only for genuinely public data; otherwise allow-list exact origins.
Reflecting arbitrary Origin + Access-Control-Allow-Credentials: trueCredentialed any-origin access. Any website can read a logged-in user's data. Critical.Validate Origin against an allow-list; only echo a single trusted origin.
Trusting Origin: nullSandboxed iframes and file:// documents send Origin: null. An attacker can force this value to bypass a naive check.Never treat the literal string null as a trusted origin.
Suffix/substring matching (e.g. trusting anything ending in yoursite.com)yoursite.com.evil.com or evilyoursite.com passes a naive check.Match the full origin string exactly, not with endsWith/includes.
Allowing the http:// version of your domain with credentialsA network attacker on plain HTTP can read credentialed responses (MITM).Allow HTTPS origins only.

SteelSuit's deep scan runs each of these probes. Its CORS checker sends requests with crafted Origin headers — including a null origin and an attacker-style arbitrary origin — and inspects the response's Access-Control-Allow-Origin, Access-Control-Allow-Credentials, -Methods, and -Headers to detect reflection. The most severe finding is the arbitrary-origin reflection paired with credentials: true, which it flags as critical; null-origin reflection with credentials is flagged high.

How do I test my CORS policy from the command line?

You can run a quick CORS checker yourself with curl. Send a request that claims to come from an origin you do not control, then read the CORS response headers.

curl -s -I \
  -H "Origin: https://evil.com" \
  https://api.yoursite.com/account

A vulnerable response reflects your forged origin back:

HTTP/2 200
access-control-allow-origin: https://evil.com
access-control-allow-credentials: true

That https://evil.com in access-control-allow-origin, especially next to access-control-allow-credentials: true, means any website can read authenticated responses from this endpoint. This is the critical case.

A safe response either omits the header entirely or returns only your real, allow-listed origin — never the forged one:

HTTP/2 200
vary: Origin

Repeat the test with -H "Origin: null" to check for null-origin trust, and with your own real origin to confirm legitimate requests still work. Because CORS headers can differ between preflight and the actual request, also try an OPTIONS preflight: add -X OPTIONS -H "Access-Control-Request-Method: GET". The official behaviour is defined in the Fetch standard's CORS protocol.

For a fuller picture across every endpoint, an external scan automates these probes. SteelSuit's CORS checker is part of its deep scan, which takes ~5–6 minutes and requires DNS-TXT domain-ownership verification (you prove you control the domain before the deeper, more intrusive tests run). The free fast scan (~26s) covers basic HTTP and security-header checks, but the crafted-origin CORS reflection testing lives in the deep scan. Either way it is a black-box scan of your live domain — no source-code access, no agent. Findings come back deduplicated, with a severity, an A–F (0–100) score, a PDF report, and a compliance mapping.

How do I fix a permissive CORS policy?

The fix is the same regardless of framework: stop trusting the Origin header as input, and decide against an explicit allow-list.

  1. Keep an allow-list of exact origins. List the full origins that may call your API — scheme, host, and port, e.g. https://app.yoursite.com. Compare the incoming Origin against that list with an exact string match.
  2. Echo back one exact origin, or nothing. If the Origin is on the list, set Access-Control-Allow-Origin to that single value (and add Vary: Origin so caches don't mix responses). If it is not on the list, send no CORS headers at all — do not fall back to * or to the reflected value.
  3. Never reflect arbitrary origins, and never trust null. Both are attacker-controllable. Reject anything not explicitly allow-listed.
  4. Match exactly — no suffix or substring checks. origin.endsWith('yoursite.com') is a vulnerability. Compare the whole string, or parse and compare host plus scheme.
  5. Only pair credentials with one trusted origin. Set Access-Control-Allow-Credentials: true exclusively when the matched origin is a single allow-listed entry — never with *.
  6. Prefer HTTPS origins. Don't allow the http:// form of your domain if the site is HTTPS.

Here is the pattern with the Node/Express cors package, where the origin callback gives you a place to enforce the allow-list:

const cors = require('cors');

const allowList = new Set([
  'https://app.yoursite.com',
  'https://admin.yoursite.com',
]);

app.use(cors({
  origin(origin, cb) {
    // No Origin header (same-origin, curl) — allow without CORS headers.
    if (!origin) return cb(null, false);
    // Exact match against the allow-list. No endsWith, no reflection of unknowns.
    return cb(null, allowList.has(origin) ? origin : false);
  },
  credentials: true, // safe: only ever paired with a single allow-listed origin
}));

Locking down CORS is one item on a broader hardening pass — it sits alongside Node.js security best practices for a deployed API and the full web application security checklist. And because the same forged-Origin requests an attacker uses to probe CORS can also surface other client-exposed weaknesses, it pairs naturally with checking for exposed API keys in your frontend.

The takeaway

CORS misconfigurations are easy to introduce — usually while trying to make credentialed cross-origin requests work — and easy to test for. Send a request with a forged Origin header and watch what comes back: if your API reflects evil.com, or trusts null, or pairs reflection with credentials: true, fix it by validating the origin against an explicit allow-list and echoing only exact, trusted values. A one-line curl confirms the problem; an allow-list and an external CORS checker confirm the fix.

Frequently asked

Is Access-Control-Allow-Origin: * dangerous?

On its own, a wildcard is usually fine for genuinely public, unauthenticated data — a public API with no cookies or auth headers. It becomes dangerous the moment the response carries anything private. Browsers refuse to combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true, so the real risk is not the literal wildcard but servers that reflect the caller's specific Origin and then add credentials: true, which produces the same any-origin access while bypassing that browser guard.

Can I use a wildcard with credentials?

No. The CORS specification forbids it: if Access-Control-Allow-Credentials is true, Access-Control-Allow-Origin must be a single explicit origin, not *. Browsers will reject the response if you try. The insecure pattern people fall into is reflecting the request's Origin header verbatim to dodge this rule — that is effectively a credentialed wildcard and is exactly what an attacker wants. Only ever send credentials: true alongside one exact, allow-listed origin.

How do I test CORS from the command line?

Send a request with a forged Origin header and inspect the response: curl -H 'Origin: https://evil.com' -I https://api.yoursite.com. If the response contains Access-Control-Allow-Origin: https://evil.com (especially with Access-Control-Allow-Credentials: true), the server is reflecting an untrusted origin and is misconfigured. A safe server omits the header or returns only its own allow-listed origin. Repeat the test with Origin: null to check for null-origin trust.

What is the safest CORS configuration?

Maintain an explicit server-side allow-list of exact origins (full scheme + host + port). For each request, compare the Origin header against that list; if it matches, echo back that one exact origin, otherwise send no CORS headers at all. Never reflect arbitrary origins, never trust the literal string null, never use suffix or substring matching, and only set Access-Control-Allow-Credentials: true when the matched origin is a single trusted entry. Prefer HTTPS-only origins.