Can another website read a site's logged-in data?
Cross-Origin Resource Sharing decides which other websites a browser will let read this one's responses. The safe default is none, and a site opts out of it deliberately. The dangerous configuration is not a wide-open one, it is a specific pair: the server repeats back whatever origin asked, and it says credentials are allowed. Together those two settings tell every browser that any website in the world may call this one with a visitor's session attached and read what comes back.
What the combination costs
Consider a visitor who is signed in and then opens an unrelated page in another tab. That page runs a few lines of JavaScript, calls the site's API, and the browser attaches the session cookie because the policy said credentials were welcome. The response arrives at the other site's script: account details, order history, the internal endpoint that returns a whole customer record. No password was guessed and no server was broken into. The browser did what it was told.
The reach is whatever the API returns to a signed-in caller. On a shop that is addresses and order history. On a dashboard it is usually everything, because internal APIs get written on the assumption that only the site's own pages call them. Anti-CSRF tokens are often readable through the same route, which turns a read into the ability to act as the visitor.
The reason it survives in production is that nothing looks wrong. The site works, the API works, no error appears in any log, and the misconfiguration is only visible from an origin nobody at the company would ever browse from. It usually arrives as a development shortcut, copied from an answer that fixed a local CORS error, and it is never revisited because it never breaks anything.
One combination, not a family of suspicions
The reason most CORS reports are noise is that the header on its own says very little. Access-Control-Allow-Origin set to an asterisk is how every public API, font service and CDN on the internet is configured, and it is safe by construction: a browser refuses to attach cookies to a request whose response allows every origin. A tool that reports the asterisk is reporting correct behaviour on a large share of the sites it scans, and teaching its readers to skip the section.
CheckWeb reports exactly one shape. The response has to repeat back the origin it was sent, and it has to allow credentials alongside it. A named origin that is not the one that asked is a whitelist doing its job, and is never reported. An asterisk without credentials is never reported. The two settings are only dangerous together, and only the pair is a finding.
The origin used to ask is a random name under .probe.invalid, a suffix reserved by RFC 2606 that can never be registered or resolved by anybody. No correctly configured server has a reason to trust it, and no site could have allowlisted it in advance, so a server that echoes it back is echoing anything it is handed. That is what makes the result a fact rather than an inference.
The request itself is an ordinary GET with one extra header. Nothing is sent that a browser would not send, no session is used, and nothing is read beyond the response headers: the check reads the policy, not the data the policy would unlock.
How it looks in the report
The finding names the address that was asked, states that the response repeated the probe origin back, and states that credentials were allowed. Both halves are in the evidence line, because either one alone is not the finding and a reader has to be able to see that both were there.
It sits at medium. A visitor has to be signed in and has to open a page under somebody else's control for the exposure to be used, which is a real sequence rather than a hypothetical one but not a single request either. The fix is a configuration change and usually a small one, which is the other half of why it is not ranked higher.
A handful of addresses are asked rather than one: the site root, the API paths the page itself references, and the common ones. A CORS policy is set by middleware and applies to whatever that middleware fronts, so the answers rarely differ, and the finding names the address it was actually observed on.
Any website can read this site's authenticated responses
This address accepts any website as a permitted origin and allows credentials with it. A site that a signed-in visitor happens to open can call it with their session attached and read the answer. Compare the incoming origin against an explicit list instead of repeating it back.
GET /api with Origin: a name that cannot exist, HTTP 200, Access-Control-Allow-Origin echoed that same origin back, with Access-Control-Allow-Credentials: true
Reflected vs listed
Reflected
The server reads the Origin header and writes it straight back into Access-Control-Allow-Origin, with Access-Control-Allow-Credentials set to true underneath. It is three lines of middleware added to make a local frontend talk to a local API, and it shipped. Every origin on the internet is permitted, including the ones that will use it.
Listed
The server holds a short list of origins it serves: the production frontend, perhaps a staging one. An incoming Origin is compared against that list, and a request from anywhere else gets no Access-Control-Allow-Origin header at all, which is what makes the browser refuse the response. Credentials are allowed only alongside a matched entry.
Where the data is genuinely public, the other correct answer is to drop the credentials setting and leave the asterisk in place. Browsers refuse to combine the two, which is exactly the protection being relied on.
How to close it
- Find where the incoming origin is being copied into the response. In Express it is usually cors({ origin: true, credentials: true }) or a hand-written middleware reading req.headers.origin. In Django it is CORS_ALLOW_ALL_ORIGINS together with CORS_ALLOW_CREDENTIALS. In Nginx it is an add_header line using the $http_origin variable. All three are the same mistake wearing different syntax.
- Replace it with an explicit list. Two or three origins is normal, they change perhaps twice a year, and comparing against a list is the entire fix. A request whose origin is not on the list must receive no Access-Control-Allow-Origin header, not an empty one and not a null one.
- Check the comparison is exact. Matching on a prefix or a suffix is how the list gets bypassed: an allowlist that accepts anything ending in example.com also accepts notexample.com, and one that accepts anything starting with https://app.example.com also accepts a longer name beginning the same way. Compare whole strings.
- Handle the preflight the same way. A browser sends an OPTIONS request before anything unusual, and a preflight handler that answers permissively while the main route is strict reopens the hole for exactly the requests that matter most.
- Then decide whether credentials are needed at all. An API that authenticates with a bearer token rather than a cookie does not need them, and dropping the setting removes the dangerous half of the pair whatever the origin rule does.
- Re-run the audit afterwards. A rule added to one route, one virtual host or one environment looks identical to a rule that works, and only a request from an outside origin shows which one shipped.
Run a free scan
The free passive scan grades headers, TLS and known CVEs on any site in about thirty seconds, with no signup. Verifying that a domain is owned adds the deep scan, which asks the site's own addresses what they would tell a website that is not theirs, and reports only the combination that is genuinely dangerous.
Check my websiteThis one needs an active scan
A passive scan reads what a site shows every visitor, and this finding is not in that layer. It takes an active audit, run by the owner on a domain they have verified, to look where it hides.
How the deep audit works →FAQ
- Is Access-Control-Allow-Origin: * a security problem?
- On its own, no. Browsers refuse to send cookies or other credentials to a response that permits every origin, so an asterisk exposes only what an anonymous visitor could already fetch. It becomes a problem only if something else, such as a bearer token in a header, is doing the authenticating.
- Why does reflecting the origin count as dangerous when a whitelist does not?
- Because reflecting means the list is every origin that exists. The response looks specific, since it names one site, but the server named whichever site asked. A whitelist refuses the ones that are not on it, and that refusal is the whole mechanism.
- Does the scan use the exposure it finds?
- No. One GET is sent with an Origin header naming a domain that cannot exist, and the response headers are read. No session is used, no data is requested, and nothing is read from the body of the answer.
- The API is behind a login. Does that not protect it?
- It is what makes the finding matter. The exposure is specifically about requests that carry the visitor's session, so an API that returns nothing to an anonymous caller returns everything to a page exploiting this one.
Related checks
- GraphQL introspectionThe other API setting checked in the same pass: an endpoint that publishes its whole schema to anyone who asks for it.
- Security headersThe response headers that decide how much a browser will let another page do with this one, and how they are graded.
- Content Security PolicyThe policy that limits what runs on the page in the first place, and why a broad one undoes most of what it is for.