All checks

CORS misconfiguration

Severity: Medium – HighQuick + Deep· 6 min read

What it is

Browsers stop one website from reading another website's responses. Cross-Origin Resource Sharing (CORS) is how your API tells the browser which other origins are allowed to read its responses anyway. A CORS misconfiguration is a policy that allows too much: typically any origin, together with credentials (the user's cookies).

Think of a receptionist told to give a client's file to "anyone who calls on the client's behalf". The calls come from the right phone, so the receptionist hands over the file, even when the caller is a stranger's website using your user's browser.

Why it matters

If your API sends Access-Control-Allow-Credentials: true and allows an origin it shouldn't, then any site your user visits can make requests to your API with the user's cookies and read the responses. That can expose account details, private data or anything your API returns to a signed-in user, without the user doing anything except opening the wrong page.

A detail matters here. Browsers refuse the literal combination of Access-Control-Allow-Origin: * with credentials, so that exact header pair doesn't leak data on its own. In practice, though, many apps "fix" the resulting error by echoing back whatever Origin the request sent. Some frameworks and libraries also turn "allow all origins" into exactly that echo. Echoing the origin with credentials is the configuration that does let any site read responses.

CORS testing is covered in the OWASP Web Security Testing Guide, and permissive cross-domain policies are tracked as CWE-942.

How it happens

Usually while fighting a CORS error in development, someone opens the policy as wide as possible, and it ships that way.

// Express + cors: "origin: true" reflects any requesting originimport cors from "cors"; app.use(cors({ origin: true, credentials: true }));

A milder version: a development origin such as http://localhost:3000 or an ngrok URL left in the production allowlist. Anyone who can serve a page from that origin gets the same access.

How it's abused

At a conceptual level:

  1. The attacker checks whether your API echoes arbitrary origins and allows credentials, by sending a request with a made-up Origin and reading the response headers.
  2. They host a page that calls your API with credentials: "include".
  3. They get a signed-in user of your app to open that page, through a link, an ad or a compromised site.
  4. The user's browser sends the request with their cookies, and because your API allowed that origin, the attacker's page reads the response.

How to fix it

List the origins you trust explicitly, and only allow credentials for those.

import cors from "cors"; const allowed = ["https://yourapp.com", "https://www.yourapp.com"]; app.use(  cors({    origin: (origin, cb) => cb(null, !origin || allowed.includes(origin)),    credentials: true,  }));

Why this works: the browser only shares a credentialed response with an origin your server explicitly names. A page on any other origin gets an error, so it can't read your users' data. The Vary: Origin header stops caches from serving one origin's CORS headers to another.

More good practice:

  • Keep dev origins out of production. Load the allowlist from configuration per environment.
  • Don't allow credentials if you don't need them. A public, read-only API can allow * safely, as long as it doesn't use cookies.
  • Remember CORS isn't access control. It only governs what browsers share. Your API still needs authentication and authorization on every route.
  • Combine with SameSite cookies, which limit when browsers send cookies on cross-site requests in the first place.

Common mistakes

  • Matching origins with includes or a loose regex. origin.includes("yourapp.com") also matches yourapp.com.attacker.example. Compare exact strings.
  • Allowing the null origin. Sandboxed iframes and some redirects send Origin: null, which attackers can produce on purpose.
  • Fixing preflight but not the actual response. Both the OPTIONS response and the real response must use the same allowlist logic.
  • Leaving a wildcard on routes that set cookies. Audit every route that responds to cross-origin requests, not just the main API.

Testing your fix

Send a request to your API with a made-up Origin header, for example with curl's -H "Origin: https://example.com", and read the response headers. A trusted origin should get Access-Control-Allow-Origin with its exact value. An untrusted one should get no CORS headers at all. Repeat for a preflight OPTIONS request.

How SetRepo detects it

CORS checks run on every Quick and Deep scan by reading your code and configuration.

  • Any origin with credentials: an any-origin or reflected-origin policy combined with credentials. That covers * with credentials, origin: true in the cors package, and code that copies the request's Origin header into the response.
  • Dev origins in production: localhost, 127.0.0.1, .local or ngrok origins hardcoded into what looks like the production allowlist.

Each finding shows the file and line with a fix prompt. When you add your live URL, SetRepo also checks your security headers and cookies, including whether cookies set Secure, HttpOnly and SameSite.

Checklist

  • The allowlist names exact origins; no wildcard when credentials are allowed.
  • The server never echoes the request's Origin without checking it against the list.
  • Development origins are not in the production allowlist.
  • Vary: Origin is set when the allowed origin depends on the request.
  • Credentials are only allowed where cookies are actually needed.
  • Every route still checks authentication and authorization.

FAQ

Is Access-Control-Allow-Origin: * always bad?

No. For a public API that doesn't use cookies or other credentials, it's fine. The risk comes from combining broad origins with credentials.

Does CORS protect my API from other servers?

No. CORS is enforced by browsers. Scripts and servers can call your API regardless, which is why authentication matters on every route.

Why did my app break after I restricted CORS?

Usually because the allowlist is missing an origin you actually use, such as www. versus the bare domain, or a preview deployment. Add the exact origins your frontend runs on.

Do I need CORS at all if my frontend and API share a domain?

If the browser loads your frontend and calls your API on the exact same origin (same scheme, host and port), CORS doesn't apply and you can leave it off. app.yourapp.com and api.yourapp.com are different origins, though, so that setup needs an explicit allowlist with just your frontend's origin.

References

Check your app for this, free.

Start free scan