dotvitals

The CORS response varies by origin but is not marked Vary: Origin

MediumConfirmedQuick winweb.cors.vary-origin-missing

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 the whole score is calculated

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.

  1. Add Origin to the Vary header on every response from the CORS-enabled routes, matched and unmatched alike.

  2. Append rather than replace, so existing Vary fields such as Accept-Encoding survive.

  3. Purge the cache after deploying — entries stored without the correct key are still wrong until they expire.

  4. 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

The configuration to publish
Vary: Origin

A 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

nginx
add_header Vary "Origin" always;
Apache
<IfModule mod_headers.c>
	Header always append Vary Origin
</IfModule>
Caddy
header Vary Origin
Express
res.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_header in a location block discards every add_header from the enclosing server block, so this line has to sit alongside the CORS headers rather than above them.

  • Shown because the response identified Apache. append rather than set, so an existing Vary: Accept-Encoding survives. Put it in the virtual host rather than .htaccess.

  • res.vary appends 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.

Open the web cors checkerBuild the fix

Other web cors checks