CORS: who is allowed to talk to your website from another site
A misconfigured sharing rule can let any website on the internet read your customers' private data. How it goes wrong and how to set it right.
The short version
The rule it relaxes
Imagine you are logged in to your online banking in one tab and you open a dodgy website in another. The dodgy page runs a script that tries to fetch yourbank.co.uk/account/balance. Your browser will happily *send* that request — with your banking cookie attached, because that is how cookies work — but it will refuse to let the dodgy page *read the response*. That refusal is the same-origin policy, and it is one of the most important security rules in the web.
CORS is how a server opts out of that protection for specific other sites. It does so with a response header: Access-Control-Allow-Origin, naming the site that is allowed to read the response. Legitimate uses are everywhere: your mobile app's backend being read by your web app on a different domain, a public API, fonts served from a CDN.
The two ways it goes badly wrong
- Reflecting the origin. The server reads the
Originheader from the incoming request and simply copies it back intoAccess-Control-Allow-Origin— so *whatever* site asks is allowed. Developers do this because it "makes CORS errors go away". Combined withAccess-Control-Allow-Credentials: true, it means any website can make a logged-in visitor's browser fetch their private data from your site and read it. This is the dangerous one. - Wildcard with credentials. Sending
Access-Control-Allow-Origin: *is fine for genuinely public data (a public API, fonts). Browsers refuse to combine*with credentials, so it cannot leak logged-in data directly — but servers that send*on endpoints that return personal data are usually one small change away from the reflection problem, and it signals the setting was never thought through.
Related sloppiness we also look for: null as an allowed origin (which attackers can trigger from sandboxed frames), and allowing origins by a loose pattern that an attacker can match with a lookalike domain such as yourbusiness.co.uk.evil.com.
What a safe configuration looks like
The principle is simple: name exactly the sites that need access, and nothing else.
- If no other site needs to read your responses — true for most business websites — do not send CORS headers at all. The default browser behaviour is the safe one.
- If a specific site of yours needs access (say
app.yourbusiness.co.ukreading fromapi.yourbusiness.co.uk), check the incomingOriginagainst a fixed allow-list and only echo it back if it matches exactly. AddVary: Originso caches do not serve one site's answer to another. - Only send
Access-Control-Allow-Credentials: trueif that other site really needs to send cookies, and never alongside a wildcard or a reflected origin. - For genuinely public, non-personal data,
Access-Control-Allow-Origin: *without credentials is correct and safe.
In PHP, a safe allow-list looks like this:
$allowed = ['https://app.yourbusiness.co.uk'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed, true)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Credentials: true');
header('Vary: Origin');
}
What VigilScan checks
We request your homepage with an Origin header set to a domain we control, and look at what comes back. If the server reflects our origin — or returns * together with credentials, or allows null — we flag it, and show you the exact headers we received so your developer can find the setting. On verified domains, deeper scans repeat this against the API-style paths we discover.
Common questions
My developer added the header to fix a "blocked by CORS policy" error. Was that wrong?
We use a CDN for images and fonts with a wildcard. Is that a problem?
Does CORS protect my site from being attacked?
Not sure if this affects your site?
Run a free check — it takes about 90 seconds and tells you exactly where you stand.