ASVS 5.0 requirement V3.4.2 says the Access-Control-Allow-Origin response header must either be a fixed value chosen by your application, or, if you build it from the request's Origin header, the origin must be checked against an allowlist of trusted origins first. If you must use Access-Control-Allow-Origin: *, the response must not contain sensitive information. It is a Level 1 requirement.
In short: never copy the Origin header straight into the response, and never use a wildcard on an endpoint that returns private data.
Why it matters
Browsers stop a page on one origin from reading responses from another, unless the other server opts in with CORS headers. The opt in is the whole control. If your server answers every origin with Access-Control-Allow-Origin: <whatever the request said> and also sends Access-Control-Allow-Credentials: true, then any website a signed in user visits can call your API with the user's cookies and read the reply. That turns a visit to a hostile page into a data leak. The weakness is catalogued as CWE-942.
Reflecting the origin is a common shortcut. A developer hits a CORS error in local development, finds an answer that says "echo the Origin header", and it ships to production. Wildcards are the other shortcut. They are safe only for public, unauthenticated data, because browsers never send credentials with a wildcard response. But an endpoint that looks public today can start returning per user fields next quarter, and nobody revisits the header.
Weak pattern matching is the third trap. Checking that the origin "ends with example.com" accepts evilexample.com. Checking that it "contains example.com" accepts example.com.attacker.net.
How to implement it
Keep an explicit list and compare whole strings.
Express:
const ALLOWED = new Set([
"https://app.example.com",
"https://admin.example.com"
]);
app.use((req, res, next) => {
const origin = req.get("Origin");
if (origin && ALLOWED.has(origin)) {
res.set("Access-Control-Allow-Origin", origin);
res.set("Vary", "Origin");
res.set("Access-Control-Allow-Credentials", "true");
}
next();
});
Flask:
ALLOWED = {"https://app.example.com", "https://admin.example.com"}
@app.after_request
def add_cors(resp):
origin = request.headers.get("Origin")
if origin in ALLOWED:
resp.headers["Access-Control-Allow-Origin"] = origin
resp.headers["Vary"] = "Origin"
resp.headers["Access-Control-Allow-Credentials"] = "true"
return resp
If only one origin needs access, use a fixed value and skip the lookup:
add_header Access-Control-Allow-Origin "https://app.example.com" always;
Three details matter. First, send Vary: Origin whenever the header changes by request, so caches do not serve one origin's reply to another. Second, compare exact scheme, host and port. Third, do not allow the string null; it is sent by sandboxed iframes and local files, and attackers can produce it on demand.
Wildcards should be reserved for genuinely public resources, such as a public font or a read only open data endpoint, and should not be combined with credentials.
How to test it
Send requests with different Origin values and look at the response:
for o in https://app.example.com https://evil.example.net null \
https://app.example.com.evil.net https://evilapp.example.com; do
echo "== $o"
curl -si https://api.example.com/v1/me -H "Origin: $o" -b cookies.txt \
| grep -i '^access-control-'
done
Only the first origin should receive an Access-Control-Allow-Origin header. The other four must come back with no CORS headers at all. If any of them is echoed, the allowlist logic is wrong.
Then check wildcards: search your configuration and responses for Access-Control-Allow-Origin: * and confirm each endpoint behind it returns only public data. Also run a request with a valid session cookie and compare the body to what an anonymous request gets. If they differ, the endpoint is not public.
A dynamic scan of your API will flag reflected origins automatically. Safeguard's API target scan tests this class of issue; the free in-browser dependency scan covers your packages, including CORS middleware with known flaws.
Common mistakes
Reflecting the Origin header. The most frequent failure, usually pasted in from a forum answer.
Suffix or substring matching. Use exact comparison against a set, or parse the URL and compare the host.
Wildcard plus credentials. Browsers refuse this combination, so developers "fix" it by reflecting the origin, which is worse.
Allowing null. Never add it to the list.
Forgetting Vary. A shared cache can serve the wrong header to the wrong site.
Different rules per environment that leak into production. A permissive development config left on a staging host that shares production data is still a problem.
Quick check
Is a wildcard fine if my API uses bearer tokens in headers rather than cookies?
It lowers the risk, since the attacker's page has no token to send. But the response must still not contain sensitive information, and any future move to cookies would make it exploitable. Prefer a fixed origin.
Why do I need Vary: Origin?
Because the response differs by request origin. Without it, a CDN may cache the reply for one origin and serve it to another.
Sources
- OWASP ASVS 5.0, requirement V3.4.2
- MDN: Cross-Origin Resource Sharing
- CWE-942 on Gold