Service workers tricks and traps

No Comments

A service worker is essentially a script running in a separate thread of the browser. It can be used to offload processing to a second thread. Typical examples are: caching assets on the client side of a web application, and also intercepting outgoing HTTP requests made by an application to serve cached responses. The browser UI thread can receive and post information to the local service worker and offload some heavy duty processing without making the UI wait for it. So a service worker can make a web application more responsive and robust in various situations. It also can reduce the load for the web servers and spare network bandwidth.

Lifecycle of a worker

I want to tell you more about possible traps related to service workers and for this it’s important to first explain the lifecycle of a service worker. A service worker of a web page can still be active in the browser even if the user has closed the tab. It also remains registered across browser sessions and after the browser is closed. When the browser is started again and the user navigates to the URL of the service worker, it can be activated again. Activation of a worker occurs when an event can be handled. It also can be terminated again by the browser when there are no events to handle. But a browser could keep the service worker idle, and running, even when no events need to be handled.

service worker lifecycle

The service worker lifecycle (Source : Service Workers: an Introduction)

The scope of a service worker is based on where the file – with the worker – is stored, so storing it in a sub folder means it can only access those files. It is important to understand that a browser on a certain page can only use a service worker belonging to the current page, also when loaded in an iframe. A service worker can use external scripts which it knows with an import like the example below.

self.importScripts('foo.js');

Service worker traps

A service worker will be kept registered after a page refresh and browser restart. It requires a page to be served via HTTPS which is a nice built-in security feature but slows down development and that’s why an exception is made for the localhost domain. This exception in itself could be an opening for exploits, one can imagine that it is possible to insert a service worker on the localhost domain with a specific port like 8080 when people run an infected web application at least once. This service worker can lie dormant after that and activate when a new web application serves from that same port. This whole chain does not present a big security risk but helps to understand how a service worker could act in an unexpected and malicious way.

Another bit of unexpected behaviour of a service worker is that when a service worker script changes and the new version is fetched from the server, the new version won’t load immediately. The update will be done if you trigger it manually or when all browser tabs are closed. In Firefox you can go to ‘about:serviceworkers’. You will see the service worker registered below and you can manually force an update or unregister. This will not force an activation for this you need to do a hard refresh at least.

Firefox sw overview

In Chrome, you can do a bit more to force activation of the service worker after an update. In the development tools it is possible to update and unregister the service worker by going to ‘Application’ and then ‘Service Workers’. The below state can be achieved when an update of the service worker is done and the service worker contains changes.

Chrome service workers status overview

Currently #218, the old service worker, is still running. We can now manually trigger the update to have effect when you click on ‘skipWaiting’.

Replacing faulty service workers

As developer sometimes you probably want your updated service worker to be effective faster than a manual hard refresh or click in developer tab. To automate that in the code, the ‘Immediate Claim’ recipe is useful. This recipe is described in the Service Worker Cookbook which is a very useful site with use case examples for service workers. An implementation of the immediate claim will trigger the ‘skipWaiting’ action when doing an install and the install is triggered by an update. A simple service worker example of the immediate claim is provided below.

self.addEventListener('install', function (event) {
        console.log('Do an action on installation.');
        return self.skipWaiting();
});
 
self.addEventListener('activate', event => {
        console.log('Do an action on activation.');
        self.clients.claim();
})

The clients.claim() line really matters on the first load of the page if you want to run the service worker before certain calls. In the below example we listen to a fetch of other.jpg.

self.addEventListener('fetch', event => {
        const url = new URL(event.request.url);
        if (url.origin == location.origin ) {
               event.respondWith(caches.match('other.jpg'));
        }
});

Without the clients.claim() we won’t see the other.jpg on the initial load. clients.claim()  also signals to the clients that this version of the service worker is now the active one.

Next to replacing a faulty service worker, another way to limit the effects of faulty service workers is to clear a locally cached service worker. This can be done by adding the Clear-Site-Data: response header which can remove a potential malicious service worker after a call to the server. A built-in mechanism of the HTTP cache is that after 24 hours the next call to retrieve a service worker file won’t use cache and will go to the server if possible.

Synergy of the worker

Service workers themselves are safe, but their persistence after page visits does pose a danger when malicious code can be injected. In this case the user could not be on the site but the service worker is actively doing unwanted behaviour like crypto-mining. I don’t want to end with this negative statement so I want to tell more about how cool service workers really are, especially with other technology like WebRTC. WebRTC is already changing the way content is delivered on the internet currently, making more use of peer-to-peer connections – although one can argue that service workers should not be used for anything that creates longer-running connections. The suggestion for allowing service workers to make use of WebRTC is some years old (https://github.com/w3c/webrtc-pc/issues/230), but still not part of browser functionality. If service workers were able to use WebRTC directly, they could intercept normal HTTP calls and handle them via a WebRTC data channel instead, and content will still be present even when the browser refreshes. This functionality would definitely make the browser experience more like a native application!

Jan Martijn Roetman

Jan Martijn Roetman is a software craftsman working at codecentric Netherlands. He is a web application developer with experience in Java and JavaScript. He is especially interested in security of applications and artificial intelligence.

Comment

Your email address will not be published. Required fields are marked *