How this check works
Scanaris requests your home page again with an Origin header from a made-up domain, as the browser would if another website tried to call you, and looks at what your server answers. If Access-Control-Allow-Origin returns that same origin and Access-Control-Allow-Credentials is true, it opens a high-severity finding: your server reflects any origin with credentials. If it answers with the * wildcard together with credentials, it opens a low-severity one: browsers reject that combination, but it reveals a configuration made blindly. Only the scanned URL is tested; if your API lives on another path or domain, test it with the command below.
What an attacker does with this
They set up a page on any domain with a fetch to your API using credentials: 'include'. When one of your users, with an open session, visits that page, the browser attaches their cookies, your server answers with the user's data and, since the CORS policy allows it, the attacker's page reads it. Profile, orders, messages, whatever the API returns. The user sees nothing: it happens in the background.
Why it happens
CORS exists so the browser blocks cross-domain reads by default. When a call fails in development (the app on one port, the API on another), the quick fix suggested by tutorials and AI tools is "allow all origins", and to make cookies work "with credentials" gets added. In production that means the protection was switched off. More detail in misconfigured CORS: the silent flaw that exposes your users.
How to fix it
- Explicit list of origins. Only the domains that genuinely call your API from the browser. In Express:
cors({ origin: ['https://app.yourdomain.com'], credentials: true }). In Next.js, in the Route Handler, compare theOriginagainst your list before returning it. In Nuxt, withrouteRulesand thecorsoption or a server middleware. - Never reflect the origin. Copying the value of
Origininto the response "because then everything works" is the exact mistake the check measures. - If you do not need cookies across domains, remove
Access-Control-Allow-Credentials. Without credentials, a*on a public read-only API is acceptable. - Add Vary: Origin when the response depends on the origin, so that no cache serves one domain the response meant for another.
- Cookies with SameSite=Lax or Strict as a second barrier: even if CORS failed, the browser would not send them from another site.
Check it is fixed
curl -sI -H "Origin: https://evil.example" https://yourdomain.com/api/me | grep -i "access-control"
The right outcome is that Access-Control-Allow-Origin does not appear for that origin, or appears with your domain and not with evil.example. Also test with your own domain in Origin to confirm your app still works.
Frequently asked questions
My API is public with no login. Is it a problem?
No, if there are no cookies or authorization headers. Access-Control-Allow-Origin: * without credentials is the correct configuration for an open API.
I have several subdomains calling the API.
List them all or validate the Origin against a strict pattern of your root domain. Never validate with "contains yourdomain.com": yourdomain.com.evil.example passes that test.
What about localhost for development?
Add it only in the development environment, through an environment variable. An http://localhost:3000 in the production list is an origin that any program on the user's computer can use.