Content-Security-Policy and service workers
Published
I was recently tripped over by a subtlety in how service worker fetch events and fetch works in conjunction with content security policy (CSP). This happened while adding an image to the about page. This post is the result of a conversation I had with Jake Archibald on twitter (with thanks for helping me to understand what was going on).
When I originally built this blog I set the content security policy (omitting policies which aren't pertinent):
Content-Security-Policy: default-src 'self'; img-src *;
This sets the policy for all requests to be limited to the same domain, except for images which may come from anywhere. I set it like this since images may come from a content delivery network (CDN). This means other domain names could be used, even for my own images.
Until now this blog has had no images at all, so no issues with this content security policy with respect to images were obvious.
When I added an image the browser refused to load it. Firefox wasn't much help here, but Chrome gave me a useful error message in the console (the URL is omitted for brevity):
Refused to connect to '<URL>' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.
This error was being thrown from a fetch performed by the service worker. With
the worker bypassed, the image loaded as expected. The error above was trying to
tell me that the request for the image within the worker was happening under
a different security policy to that expected. Specifically, the worker is using
the connect-src
policy when performing the request for the image, and not the
image-src
policy I expected. connect-src
is the policy used by scripts
making requests. Since I don't define a connect-src
policy, the fallback is
default-src
, which is limited to the domain of the site, and does not allow an
image to be downloaded from a CDN.
There is a quick solution, which is to add a connect-src *;
policy. By
limiting this policy to the service worker, no other scripts will get to make
requests to anywhere. The Netlify config for this looks like:
[[headers]]
for = "/sw.js"
[headers.values]
Content-Security-Policy = "connect-src *;"
But what's actually going on here? I was confused because I had expected
the fetch performed inside the worker to be subject to the image-src
policy.
I even checked that the initiator and destination of
the request in the fetch event handler were for an image.
The fetch event and handler looks something like the following:
addEventListener('fetch', fetchEvent => {
const responseFromFetch = fetch(fetchEvent.request);
// Other stuff omitted...
});
I'm effectively proxying the request. I've omitted a bunch of stuff to do with caching.
When fetch is called with a request object, the URL of the request is used to
make an entirely new request before processing it. This new request
lacks information about the initiator and destination, and so it is subject to
the connect-src
policy.
This seemed bad to me at first. CSP being different with and without a service worker would be bad because you'd have to test both each time a new resource type is added.
Fortunately, it turns out that the browser also performs CSP checks on the request before the service worker receives the fetch event and on the response it receives from the service worker (important if the worker changes a URL, which I'm not doing). Restating for the example of an image, these three checks are made:
- The initial request for is checked for
image-src
violation. - A request with the same URL is checked in the service worker for
connect-src
violation. - The response from the service worker is checked for
image-src
violation.
This means that I'm safe with the connect-src *;
policy for the service worker
mentioned above, since the browser was already applying the image-src
policy
to image requests before the service worker saw them!
Mentions
Mention from Nicolas Hoizey on : TIL: if you cache images with a Service Worker, and you have a Content Security Policy, the image's origin should be in both the img-src and connect-src directives...