I am designing a JavaScript secure loader. The loader is inlined in the index.html
. The goal of the secure loader is to only load JavaScript resources are trusted. The contents of index.html
are mostly limited to the secure loader. For security purposes, I want index.html
(as stored in cache) to never change, even if my website is hacked.
How can I cache index.html
without the server being able to tamper with the cache? I am wondering if ServiceWorker
s can help. Effectively, the index.html
would register a service worker for fetching itself from an immutable cache (no network request is even made).
Service workers are specialized JavaScript assets that act as proxies between web browsers and web servers. They aim to improve reliability by providing offline access, as well as boost page performance.
This means each browser vendor decides cache limits and invalidation strategy. Up to this point Apple has decided to limit the service worker cache storage limit to roughly 50MB. It is actually limited to 50MiB, which equates to about 52MB.
We ll put the request into the caches - caches. put with the response clone returned from the fetch promise. Return the response. If promise got rejected fetch will go to caches (pre-cache) to match the request URL request.
Service workers will register and work just fine as long as you access the localhost domain -- without HTTPS.
in chrome you can use FileSystem API
http://www.noupe.com/design/html5-filesystem-api-create-files-store-locally-using-javascript-webkit.html this allows you to then save and read files from a sand-boxed file-system though the browser.
As for other support it's not been confirmed as an addition to the HTML5 specification set yet. so it's only available in chrome.
You could also use the IndexDB system this is supported in all modern browsers.
you can use both these services inside a Service Worker to manage the loading and manage of the content however i have to question why would you want to you prevent your self from ever updating your index.html
This design goal of "secure javascript loading"/TOFU is typically associated with javascript crypto and browser secrets (e.g. Cyph, Mega), so I'll include some relevant recommendations along the way.
You're in dangerous territory. There are many dragons.
Option 1: Implement feross/infinite-app-cache to permanently cache the app in the browser
Here's the least amount of code required to achieve a TOFU web app in all browsers.
index.html:
<html manifest="manifest.appcache">
manifest.app cache:
CACHE MANIFEST
/manifest.appcache
/index.html
/script-loader.js
And make sure you serve the manifest with a content type of "text/cache-manifest".
Note: This standard is deprecated, however it will take some years before browsers disable this feature.
Problem: If an attacker (who has already hacked your server) can trick your users into visiting a URL not in the app cache, they can serve arbitrary code to extract/use browser secrets etc.
Solution: User input cannot be intercepted by a malicious page on the same domain (unless you do something really silly), so encrypt any browser secrets using a user-supplied password and dchest/scrypt-async-js.
Another thing to consider is the cached contents of a page CAN be extracted by the malicious page simply by using AJAX - so you HAVE to use user input, not just a random token rendered to the page.
Option 2: Extend the above solution with HPKP Suicide and a Service Worker
Implement HPKP Suicide to permanently "bake" the app into the browser by bricking the user's connection to the server.
If (and only if) you have implemented HPKP Suicide, service workers are now safe to use because the enforced max-age of 24 hours has no effect if the browser can't re-download the service worker.
index.html or script-loader.js:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js').catch(function (error) {
console.error(error);
});
});
}
service-worker.js:
var files = [
'/',
'index.html',
'script-loader.js'
].map(file => new Request(file))
self.addEventListener('install', () => {
caches.open('cache')
.then(cache => {
files.map(file => {
fetch(file)
.then(response => cache.put(file, response))
.catch(err => console.error(err))
})
})
.catch(err => console.error(err))
})
self.addEventListener('fetch', (e) => {
var url = e.request.url.split('#')[0]
if (!files.filter(file => file.url === url).length) {
return e.respondWith(new Response('Not Found', { status: 404 }))
}
return e.respondWith(
caches.match(e.request).then(cachedResponse => {
if (cachedResponse) return cachedResponse
return Promise.all([
caches.open('cache'),
fetch(e.request.clone())
]).then((results) => {
var cache = results[0]
var response = results[1]
cache.put(e.request, response.clone())
return response
})
})
)
})
Important note: Without HPKP Suicide, using a service worker is LESS secure than infinite app cache because the service worker has a max-age of 24 hours, even if your server's cache directives set a higher age. Using service workers will also completely disable app cache. It's a waste of time trying to achieve this with service workers if you haven't implemented HPKP Suicide.
Discussion
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With