The CORS response varies by origin but is not marked Vary: Origin
What this check looks for
The allowed-origin header changes depending on who asks, but the response does not tell caches that. A cache can store one visitor's answer and give it to somebody from a different site.
Why it matters
A correct allow-list stops being correct at the cache. A CDN, a reverse proxy or even the visitor's own browser cache can keep the response it fetched for an allowed origin and replay it — allow-list header and all — to a site that is not on the list. Nobody changed the configuration; the cache did it.
When the check passes, your report says: “The response is marked Vary: Origin, so caches keep answers apart”.
What it costs your score
When this check fails it removes 8 points from your Web security score, before the status, confidence and repeat multipliers are applied. Web security carries a weight of 8 in the overall score.
It shares the web-security.cors family ceiling of 35 points: however many findings that family produces, together they cannot remove more than that from Web security. One underlying problem showing up in several places is still one problem.
- Severity
- medium
- Default confidence
- confirmed
- Status when triggered
- warn
- Deduction
- 8 points
- Family cap
- web-security.cors · 35
- Category
- Web security
- Module
- Web cors
- Fix owned by
- user
- In the ruleset since
- 2026.09
How to fix it
Send Vary: Origin on every response whose CORS headers depend on the request origin.
Without it a shared cache can hand one origin's permissive response to another.
Add
Originto theVaryheader on every response from the CORS-enabled routes, matched and unmatched alike.Append rather than replace, so existing
Varyfields such asAccept-Encodingsurvive.Purge the cache after deploying — entries stored without the correct key are still wrong until they expire.
Check any CDN in front as well: some strip or rewrite
Vary, and a policy that is correct at the origin can still be cached wrongly at the edge.
How to confirm it worked
curl -sSI -H 'Origin: https://app.example.com' https://‹host›/ | grep -i '^vary:' — expect Origin to be listed
curl -sSI -H 'Origin: https://not-your-site.example' https://‹host›/ | grep -i '^vary:' — expect Origin here too
Vary: OriginA named slot like ‹domain› — and the braces left in the configuration below — is filled in with your own values when this rule appears on a report.
Remediation by platform
add_header Vary "Origin" always;<IfModule mod_headers.c>
Header always append Vary Origin
</IfModule>header Vary Originres.vary('Origin');Set it on every response from the location, not only on the ones where the origin matched — the unmatched response is the one a cache would otherwise reuse. Watch the inheritance rule: an
add_headerin alocationblock discards everyadd_headerfrom the enclosing server block, so this line has to sit alongside the CORS headers rather than above them.Shown because the response identified Apache.
appendrather thanset, so an existingVary: Accept-Encodingsurvives. Put it in the virtual host rather than .htaccess.res.varyappends rather than replacing, so other Vary fields are kept.
Technical detail
Access-Control-Allow-Origin changed between our probes but no Vary: Origin was present on the response from ‹origin›. ‹detail›
Vary is the list of request headers a cache must include in its cache key. Without Origin in that list, the stored entry is keyed on the URL alone, so the first requester's Access-Control-Allow-Origin is what every later requester receives from that cache. Two directions of failure follow, and both are real: an allowed origin's permissive response gets served to a disallowed one, and a disallowed origin's blocked response gets served to an allowed one — which shows up as an integration that works for some users and not others, seemingly at random.
This matters *because* the policy is dynamic. A response with a static Access-Control-Allow-Origin: * does not need it, and this rule does not fire on that case. Send it unconditionally on the responses that do vary — including the ones where the origin did not match, since those are precisely the entries a cache would otherwise reuse.
On Apache use Header always append Vary Origin rather than set, so an existing Vary: Accept-Encoding is not overwritten. Detected server: ‹detected server›.
Standards and references
Test this on your domain
Run the check that produces this finding, on its own, against any domain.
Other web cors checks
- The CORS policy accepts any request header from any origin
- The Access-Control-Allow-Origin value is not a valid origin
- The CORS policy grants broad methods on an unrestricted origin
- The null origin is on the CORS allow-list
- Any website is allowed to read this site's responses
- Any website can read authenticated responses from this site
- Preflight results are not cached
- Resources are shared with every origin, without credentials
- The CORS policy pairs a wildcard origin with credentials