Qubyte Codes

Content-Security-Policy and service workers

#AboutThisBlog #JavaScript

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 look 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!