Asset Caching with Service Workers Considering Potential Security Vulnerabilities
Asset caching is a critical component in web development, particularly for Progressive Web Apps (PWAs). The service worker is a vital web technology for enhancing web applications, enabling features like offline access, push notifications, and background synchronization. However, they also introduce certain security vulnerabilities that developers must manage carefully. Having implemented this for many web applications in my professional journey, I am motivated to share my insights in this article. I refrain from explaining what PWAs or Service Workers are, as I assume you have some fundamental knowledge of them. Instead, I will exemplify some working code snippets and explain security complications and related remediations. So, let's dive deep and understand this important concept of service workers.
If you find it insightful and appreciate my writing, consider following me for updates on future content. I'm committed to sharing my knowledge and contributing to the coding community. Join me in spreading the word and helping others to learn. Follow WebWiz: https://www.dhirubhai.net/newsletters/webwiz-7178209304917815296jur3il4jlk
Asset Caching Strategies
When implementing asset caching with service workers, developers can choose from several caching strategies:
1. Cache First
This strategy serves cached content first and falls back to the network if the content is not available in the cache. This is ideal for assets that do not change frequently, such as images and stylesheets.
2. Network First
This strategy attempts to fetch the latest content from the network before falling back to the cache. This is suitable for dynamic content that needs to be updated regularly.
3. Stale While Revalidate or Eventually Fresh
This is a balanced approach that serves cached content immediately while simultaneously fetching updated content from the network. This ensures that users receive a fast response while also getting the latest updates. In this article, I will try to explain this particular asset caching technique with some code snippets.
4. Pre-caching
The application can preemptively cache specific assets that are likely to be requested, ensuring that they are available offline. This requires careful planning to determine which assets are essential for offline functionality.
Benefits of Asset Caching
Implementing asset caching with service workers provides numerous advantages:
领英推荐
Potential Security Vulnerabilities
While service workers offer many benefits, they also introduce many potential security vulnerabilities that developers must address. I would like to highlight some of the major risks as follows. If you want to enrich your knowledge about various security risks and related measures, you may find this page insightful: https://cheatsheetseries.owasp.org/index.html
Secured Code Snippets of "Eventually Fresh" Asset Caching Pattern
As always, I think code snippets are more explanatory than just raw theories for those who are developers at heart like me. So, let me share some modified fragments of production-grade secured code that I wrote earlier in some projects. You can't copy-paste this for sure, but fundamentally, you'll get the essence of how it should be managed.
Install Event Handler
self.addEventListener("install", (event) => {
console.log('WORKER: install event is in progress.');
event.waitUntil(
caches
.open('basic')
.then((cache) => {
/* After the cache is opened, we can fill it with the offline data. The below method will add all resources we've indicated to the cache, after making HTTP requests for each of them */
return cache.addAll([
'/',
'/css/some/global.css',
'/js/some/global.js'
]);
})
.then(() => {
console.log('WORKER: installation is completed');
})
);
});
Let me explain the above snippet. addEventListener function is used to register an event handler for the install event. Using `event.waitUntil` blocks the installation process on the provided promise. If the promise is rejected because, for instance, one of the resources failed to be downloaded, the service worker won’t be installed. Here, you can leverage the promise returned from opening a cache by calling `caches.open(name)` and then mapping that into, `cache.addAll(resources)` function which downloads and stores responses for the provided resources.
Intercepting Fetch Requests
The fetch event fires whenever a page controlled by this service worker requests a resource. This isn't limited to fetch or even XMLHttpRequest. It includes requests for the HTML page on the first load, as well as JS and CSS resources, fonts, images, etc. Additionally, requests made against other origins will also be caught by the fetch handler of the ServiceWorker.
In the following example, I'm using an "Eventually Fresh" caching pattern. If you want to read more about it, visit this page https://ponyfoo.com/articles/progressive-networking-serviceworker. Here, I return whatever is stored in the cache but always try to fetch the resource again from the network, regardless, to keep the cache updated. If the response we served to the user is stale, it will get a fresh response the next time they request the resource. If the network request fails, it will attempt to recover by serving a hardcoded Response.
self.addEventListener("fetch", (event) => {
console.log('WORKER: fetch event is in progress.');
const { request } = event;
if (request.method !== 'GET') {
console.log('WORKER: fetch event ignored.', request.method, request.url);
return;
}
// it blocks the fetch event on a promise
event.respondWith(
caches
.match(request)
.then((cached) => {
/* Even if the response is in our cache, it goes to the network as well.
This pattern is known for producing "eventually fresh" responses,
where we return cached responses immediately, and meanwhile pull
a network response and store that in the cache */
const networked = fetch(request)
.then(fetchedFromNetwork, unableToResolve)
.catch(unableToResolve);
console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', request.url);
return cached || networked;
function fetchedFromNetwork (response) {
/* It copies the response before replying to the network request. This is the response that will be stored on the ServiceWorker cache */
const cacheCopy = response.clone();
console.log('WORKER: fetch response from network.', request.url);
caches
.open('pages')
.then((cache) => {
/* We store the response for this request to serve later if it is matched by caches.match(request) */
cache.put(request, cacheCopy);
})
.then(() => {
console.log('WORKER: fetch response stored in cache.', request.url);
});
return response;
}
/* When this method is called, it means we were unable to produce a response
from either the cache or the network */
function unableToResolve () {
console.log('WORKER: fetch request failed in both cache and network.');
return new Response('<h1>Service Unavailable</h1>', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
})
);
});
Phasing Out Older ServiceWorker Versions
Now, we have to delete old caches that don’t match the version for the worker has just finished installing itself.
self.addEventListener("activate", (event) => {
// event.waitUntil blocks activate on a promise.
console.log('WORKER: activate event is in progress.');
event.waitUntil(
caches
.keys()
.then((keys) => {
// We return a promise that settles when all outdated caches are deleted.
return Promise.all(
keys
.filter((key) => {
// Filter by keys that don't start with the latest version prefix.
return !key.startsWith(version);
})
.map((key) => {
// Return a promise that's fulfilled when each outdated cache is deleted
return caches.delete(key);
})
);
})
.then(() => {
console.log('WORKER: activation is completed.');
})
);
});
I hope you found this article insightful and took away some valuable learnings to implement in your professional or personal projects. I'd love to hear about your experiences with asset management using Service Workers, especially if you've encountered any security issues and how you resolved them. Stay tuned for my next article next week, where I'll dive into another exciting topic!