Yesterday, I started looking a site compatibility bug where a page’s layout is intermittently busted. Popping open the F12 Tools on the failing page, we see that a stylesheet is getting blocked because it lacks a CORS Access-Control-Allow-Origin response header:
We see that the client demands the header because the LINK element that references it includes a crossorigin=anonymous directive:
crossorigin="anonymous" href="//s.axs.com/axs/css/90a6f65.css?4.0.1194" type="text/css" />
Aside: It’s not clear why the site is using this directive. CORS is required to use SubResource Integrity, but this resource does not include an integrity attribute. Perhaps the goal was to save bandwidth by not sending cookies to the “s” (static content) domain?
In any case, the result is that the stylesheet sometimes fails to load as you navigate back and forward.
Looking at the network traffic, we find that the static content domain is trying to follow the best practice Include Vary: Origin
when using CORS for access control.
Unfortunately, it’s doing so in a subtly incorrect way, which you can see when diffing two request/response pairs for the stylesheet:
As you can see in the diff, the Origin token is added only to the response’s Vary directive when the request specifies an Origin header. If the request doesn’t specify an Origin, the server returns a response that lacks the Access-Control-* headers and also omits the Vary: Origin header.
That’s a problem. If the browser has the variant without the Access-Control directives in its cache, it will reuse that variant in response to a subsequent request… regardless of whether or not the subsequent request has an Origin header.
The rule here is simple: If your server makes a decision about what to return based on a what’s in a HTTP header, you need to include that header name in your Vary
, even if the request didn’t include that header.
If your response specifies Vary: Origin
to ensure that the browser rechecks with you before reusing a response, your server had better not return a HTTP/304 Not Modified
response to a request from a different Origin without *also* updating the Access-Control-Allow-Origin
to match the new request context. Otherwise, your cached response is not going to be readable in that new context.
-Eric
PS: This seems to be a pretty common misconfiguration, which is mentioned in the fetch spec:
CORS protocol and HTTP caches
If CORS protocol requirements are more complicated than setting `Access-Control-Allow-Origin` to * or a static origin, `Vary` is to be used.
Vary: Origin
In particular, consider what happens if Vary
is not used and a server is configured to send Access-Control-Allow-Origin
for a certain resource only in response to a CORS request. When a user agent receives a response to a non-CORS request for that resource (for example, as the result of a navigation request), the response will lack Access-Control-Allow-Origin
and the user agent will cache that response. Then, if the user agent subsequently encounters a CORS request for the resource, it will use that cached response from the previous non-CORS request, without Access-Control-Allow-Origin
.
But if Vary: Origin
is used in the same scenario described above, it will cause the user agent to fetch a response that includes Access-Control-Allow-Origin
, rather than using the cached response from the previous non-CORS request that lacks Access-Control-Allow-Origin
.
However, if Access-Control-Allow-Origin
is set to *
or a static origin for a particular resource, then configure the server to always send Access-Control-Allow-Origin
in responses for the resource — for non-CORS requests as well as CORS requests — and do not use Vary
.
They’re probably putting crossorigin=”anonymous” so they can get stack traces from javascript crashes.
Interesting… Does this do anything fun/interesting for stylesheets? I guess maybe it would allow you to enumerate the sheet’s rules?
No idea!