# CORS settings that let other sites read your API

> CORS only decides which web pages may read a response in the browser. Reflected origins, wildcards with cookies, null and sloppy patterns undo that decision.

Published 2026-10-27 · https://aiskills402.com/blog/cors-config-mistakes

Cross-origin resource sharing is a permission slip that a server hands to browsers, saying which other web pages may read its responses. It does not keep anyone out of your server, it does not authenticate a caller, and it does nothing at all against a script or a command-line tool. What it guards is narrow and real: a page on one site should not be able to use a visitor's logged-in browser to read that visitor's private data from another site. Almost every serious misconfiguration is a way of handing out that slip to the wrong page. This guide explains what is being protected, then walks through the settings that give the protection away, and ends with a short check you can run against your own server.

## What the browser does, and what it leaves alone

Browsers follow a same-origin policy: a script on one origin may not read the response of a request made to another origin. An origin is the combination of scheme, host and port. CORS is the controlled exception, a way for the server to say that a particular other origin may read the answer ([MDN, CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)).

Three facts shape everything that follows.

First, the check happens in the browser, on the response. For many requests the server still receives and processes them; only the page's ability to read the result is withheld. MDN notes that requests which look like a plain form submission need no opt-in from the server, since the cross-site risk there is no worse than a form post ([MDN, CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)). So CORS is not a fix for cross-site request forgery, and a state-changing endpoint still needs its own defences.

Second, cookies are not sent on cross-origin calls unless the calling code opts in, and the server must also agree. The browser rejects a response to a credentialed request unless it carries the header that allows credentials, and it hides the reply from the page ([MDN, CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)). Credentials are where the stakes are, because a response made with someone's session is the thing worth stealing.

Third, the `Origin` request header is only trustworthy as a label for the browser's own behaviour. OWASP points out that any other client, such as curl or an intercepting proxy, can put whatever it likes in that header, so it is not suitable for proving a request comes from your site ([OWASP, CORS OriginHeaderScrutiny](https://owasp.org/www-community/attacks/CORS_OriginHeaderScrutiny)). The remedy OWASP gives is ordinary authentication on the resource. CORS sits on top of authentication; it never replaces it.

## Mistake one: echoing the caller back as the allowed site

The most common shortcut is a handler that takes whatever `Origin` header arrived and writes that same value back as `Access-Control-Allow-Origin`, a reflected Origin, often together with `Access-Control-Allow-Credentials: true`. It is popular because it makes every browser error vanish at once. It is also equivalent to telling every website on the internet that it may read your signed-in users' data. A hostile page just has to make the browser call your API, and the server will approve it by echoing its own address back.

The fix is an allow-list. Keep an explicit set of full origins (scheme, host and port), compare the incoming value to the set by exact string equality, and answer with the matching entry. If there is no match, send no allow header at all. Never fall back to the first entry as a default, because that confuses debugging and teaches nobody anything.

## Mistake two: a wildcard together with credentials

The wildcard value means any origin. MDN states that for credentialed requests the server must name a specific origin rather than the wildcard, and that the browser blocks access to the response when a request carries a cookie and the answer carries the wildcard ([MDN, CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)). In that sense the standard saves you: the combination fails. The trouble is what developers do next. Seeing the browser refuse, they swap the wildcard for the reflection from mistake one, and the error goes away because the door is now open. The wildcard itself is fine for a genuinely public, read-only resource that uses no cookies and returns nothing personal. Keep the two situations apart in your code: public data may use the wildcard, private data uses the allow-list.

## Mistake three: trusting the null origin

Sometimes the browser sends `Origin: null`. MDN explains that this value appears for opaque or privacy-sensitive origins, including documents loaded from local files, data URLs and some other schemes ([MDN, the Origin header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin)). Developers meet it while testing a page opened straight from disk and add `null` to the allow-list to make the problem go away. The trouble is that an attacker can also produce a null origin, for example from a sandboxed frame or a data document they control, so allowing it grants access to anyone willing to create one. Test your front end from a real local server on a real origin, and keep `null` out of the list in every environment that can be reached from outside.

## Mistake four: patterns that match more than you meant

Allow-lists drift into pattern matching once there are many subdomains or preview deployments. Three habits cause most of the damage:

- Checking that the origin ends with your domain. An attacker registers a name that merely ends with the same letters, and the check passes.
- Checking that your domain appears somewhere inside the origin. A hostile site can place your name in a subdomain or a path-like part of its own host.
- A hostname pattern that lacks anchors, or that uses a bare dot, so that a single character matches anything.

If wildcards of subdomains are truly needed, parse the origin into scheme and host first, require the scheme to be https, and compare the host with either an exact entry or a dot followed by your registered domain as the final part of the host. Better still, list the actual hostnames. Also remember that every subdomain you trust becomes part of your attack surface, so a forgotten or hijacked one can read what your main API returns.

## Mistake five: a per-origin answer without Vary

When the allow header changes with the requester, the response differs by `Origin`. A shared cache between you and the visitor, or the browser's own cache, may store the answer for one origin and replay it to another, either breaking a legitimate site or, worse, showing one origin's permission to a different caller. MDN's guidance is that a server which names a single origin dynamically, from an allow-list, should list `Origin` among the values of its `Vary` response header, which tells clients the reply changes with that request value ([MDN, CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)). The same advice applies to the answer to the preflight request. This is easy to forget because nothing fails on your laptop: there is no cache between you and the server. It shows up only behind a content delivery network, and then it looks random.

## A short working setup

Putting the pieces together, a safe default looks like this, whatever framework you use:

1. Keep one list of exact allowed origins in configuration, not scattered through handlers.
2. On each request, compare the incoming `Origin` to that list with plain equality.
3. If it matches, return that origin, return `Vary: Origin`, and add the credentials header only for endpoints that truly need cookies.
4. If it does not match, return no CORS headers and let the browser refuse.
5. Allow only the methods and request headers your front end uses, and cache preflight answers for a modest time.
6. Keep authentication and authorisation on every private endpoint, as if CORS did not exist.

To see whether your server really behaves this way, send it a request from the command line with an `Origin` header of a made-up site and look at the reply headers. If the made-up site is echoed back, or the answer lacks `Vary: Origin` while the allow header is present, you have found a problem. Repeat with the null origin and with a lookalike hostname that ends with your real domain.

## When this does not apply

This guide covers the browser-enforced model for ordinary web pages. It does not cover server-to-server calls, mobile apps or command-line clients, where CORS is not enforced and your own authentication is the only barrier. It does not apply to resources that are public on purpose and carry no credentials, where the wildcard is the correct answer. Some gateways and frameworks add or strip these headers themselves, so the headers you see on the wire may differ from the ones in your handler; always test the deployed address. Browsers also change details over time, such as how third-party cookies are treated, and the pages we cite are the place to confirm current behaviour. Finally, we did not run new experiments for this guide: it restates public documentation and the common patterns found in code review, so treat the examples as illustrations, not measurements.

Trust decisions about outside code have the same shape as the ones above, and our article on [installing agent skills you have not vetted](/blog/agent-skills-security-risks) walks through the risks of that choice.

## Checking your own configuration

If you would like a quick second look at a CORS setup before release, our [cors-config-review](https://aiskills402.com/skills/cors-config-review) skill reads the cross-origin part of a handler, a gateway block or a framework setting and lists only the mistakes described above: an echoed origin with cookies allowed, a wildcard used with credentials, the null origin accepted, a loose pattern for matching origins, and a missing Vary header. If the setup is sound it says so in a single line. The skill is one entry in the store at https://aiskills402.com.
