Background

The Service Worker specification introduces a powerful worker to better control network requests. Service workers can intercept fetch events and decide what to respond with. The specification also includes a Caches API for websites to store response cache. Site owners can easily benefit from the two new APIs that boost page load speed and optimize user experience since service workers can serve static resources or even the initial document request.

Nonetheless, Caches API is somewhat fragile and vulnerable to XSS attacks. It can be harnessed by malicious code to pollute caches, which may enable XSS to survive page reloading or deletion from backend databases and lead to persistent threats to users. We will demonstrate several possible cases in the following article. Fortunately, the specification is relatively new and still in the Candidate Recommendation stage; top websites are still making careful progress in shipping caches to production. We have time to figure out ways to counteract.

Details

The exploitable part in the spec locates at § 5.4. Cache, stated by the following description.

§ 5.4. Cache

[SecureContext,Exposed=(Window,Worker)]
interface Cache {
  [NewObject] Promise<any>match(RequestInforequest, optionalCacheQueryOptionsoptions = {});
  [NewObject] Promise<FrozenArray<Response>>matchAll(optionalRequestInforequest, optionalCacheQueryOptionsoptions = {});
  [NewObject] Promise<void>add(RequestInforequest);
  [NewObject] Promise<void>addAll(sequence<RequestInfo>requests);
  [NewObject] Promise<void>put(RequestInforequest,Responseresponse);
  [NewObject] Promise<boolean>delete(RequestInforequest, optionalCacheQueryOptionsoptions = {});
  [NewObject] Promise<FrozenArray<Request>>keys(optionalRequestInforequest, optionalCacheQueryOptionsoptions = {});
};

[Cache](<https://www.w3.org/TR/service-workers/#cache>) object represents a request response list. Multiple separate objects implementing the [Cache](<https://www.w3.org/TR/service-workers/#cache>) interface across documents and workers can all be associated with the same request response list simultaneously.

As every document can access and operate the cache object, which stores the same content as the service worker, it’s possible for a malicious script to write contents into caches. Because there is no mechanism to verify the caches are valid and secure server response, they can then be served as trusted ones by innocent service workers.

Exploitation Cases

To begin with, we can write a simple victim service worker that serves static resources of all kinds from caches. Here is a code snippet from MDN illustrating basic usage of CacheStorage API.

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('v1').then(function(cache) {
      return cache.addAll([
        '/sw-test/',
        '/sw-test/index.html',
        '/sw-test/style.css',
        '/sw-test/app.js',
        '/sw-test/image-list.js',
        '/sw-test/star-wars-logo.jpg',
        '/sw-test/gallery/bountyHunters.jpg',
        '/sw-test/gallery/myLittleVader.jpg',
        '/sw-test/gallery/snowTroopers.jpg'
      ]);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(caches.match(event.request).then(function(response) {
    // caches.match() always resolves
    // but in case of success response will have value
    if (response !== undefined) {
      return response;
    } else {
      return fetch(event.request).then(function (response) {
        // response may be used only once
        // we need to save clone to put one copy in cache
        // and serve second one
        let responseClone = response.clone();

        caches.open('v1').then(function (cache) {
          cache.put(event.request, responseClone);
        });
        return response;
      }).catch(function () {
        return caches.match('/sw-test/gallery/myLittleVader.jpg');
      });
    }
  }));
});

The service worker functions as follows:

Overwrite static resources

Static resources bear the brunt of cache pollution. Malicious scripts can not only change the contents of stylesheets or API responses, but also write arbitrary code to static Javascript resources in CacheStorage simply by constructing a Response object.

const c = await caches.open('cache-name')
c.put('index.js', new Response('alert(1)'))

Overriding unexpected files

Scripts may as well control other resources beyond our expectations.

Content Security Policy Bypassing

Content Security Policy Level 3 specifies two methods CSP can be delivered to clients, as described in § 3. Policy Delivery. A server can enable CSP via some specific HTTP header or HTML document with an extra meta tag.