Application Security

PWA Service Worker Attack Surface: What Security Teams Overlook

Service workers give Progressive Web Apps powerful offline and caching capabilities, but they also create a persistent attack surface that outlives the browser tab. Understanding this surface is critical.

Alex
Security Researcher
7 min read

Service workers are the backbone of Progressive Web Apps. They sit between the application and the network, intercepting every fetch request and deciding how to fulfill it. This position gives them extraordinary power -- they can serve cached content, modify responses, redirect requests, and synchronize data in the background. They persist beyond the browser tab, running in their own thread with their own lifecycle.

This persistence and power is exactly what makes service workers a compelling attack surface. A compromised service worker is not a one-time exploit. It persists in the browser's service worker registration, intercepting requests until it is explicitly unregistered or replaced. If an attacker can control a service worker, they have a persistent man-in-the-middle position inside the user's browser.

How Service Workers Create Persistent Risk

A service worker, once registered, remains active for the scope it controls (typically the entire origin). It survives page refreshes, tab closes, and even browser restarts. The browser activates it when a navigation request falls within its scope.

This persistence means that a malicious service worker installed through an XSS vulnerability continues operating after the XSS vector is patched. The vulnerable page that registered the service worker might be fixed, but the service worker it registered is still sitting in the browser, intercepting every request to the origin.

The only ways to remove a registered service worker are: the user manually clears site data, the server pushes an update that unregisters the worker, or the browser expires it (which varies by browser and is measured in days, not minutes).

Cache Poisoning Attacks

Service workers control the Cache API, which stores request/response pairs. A compromised service worker can poison this cache with modified responses. The browser serves the cached responses without verifying them against the server, because caching is the entire point.

Scenario: an attacker compromises a service worker and modifies the cached version of your main.js bundle to include a keylogger. Every subsequent page load serves the poisoned JavaScript from cache. The user sees valid HTTPS with your domain. Nothing looks wrong. The original server is never contacted because the cache hit is served directly.

Cache poisoning through service workers is particularly dangerous because it survives across sessions and is invisible to server-side logging. The poisoned responses are served locally -- your server never sees the requests because the service worker intercepts them before they reach the network.

Service Worker Update Mechanisms

Service workers update when the browser detects that the service worker script has changed (a byte-for-byte comparison). The browser downloads the new script, installs it, and transitions from the old worker to the new one. This update mechanism is both a defense and an attack surface.

As a defense, pushing a new service worker version can remediate a compromised worker. The new version replaces the old one during the next update check.

As an attack surface, the update mechanism itself can be exploited. If an attacker can modify the service worker response during an update check (through a compromised CDN, DNS hijacking, or a proxied connection), they can push a malicious worker update. HTTPS mitigates network-based modification, but does not protect against CDN compromise.

The updateViaCache option controls whether the HTTP cache is used during service worker update checks. Setting it to 'none' ensures the browser always fetches the service worker script from the server, bypassing potentially stale or poisoned HTTP caches. This is a security-relevant configuration.

Scope and Registration Risks

A service worker's scope determines which requests it intercepts. By default, the scope is the directory containing the service worker script. A service worker at /app/sw.js controls all requests under /app/.

Overly broad scopes increase the attack surface. A service worker registered at the root (/) intercepts every request to the origin. If your application only needs offline support for the /app/ path, register the service worker there -- not at the root.

Service worker registration requires the service worker script to be served with the correct MIME type (text/javascript or application/javascript) from the same origin. An attacker cannot register a service worker from a different origin. But if they can inject a script on your origin that calls navigator.serviceWorker.register(), they can register a service worker they control.

The Service-Worker-Allowed response header can extend the allowed scope beyond the script's directory. Be cautious with this header. Granting a service worker permission to control a broader scope than necessary increases the blast radius of a compromise.

Third-Party Script Risks

PWAs often include third-party scripts: analytics, advertising, customer support widgets, A/B testing frameworks. These scripts execute on your origin and can interact with service workers.

A compromised third-party script can register a new service worker or interfere with your existing one. If the third-party script is loaded from your origin (self-hosted or proxied), it has full access to the service worker API. Even scripts loaded from external origins can interact with service workers through your page's JavaScript context.

Content Security Policy can restrict which scripts execute on your pages, but once a script is allowed to execute, there is no CSP directive that specifically prevents service worker registration. The worker-src directive controls which URLs can be used as workers, but if an attacker can host a script on your origin, worker-src 'self' does not help.

Background Sync and Push Exploitation

Service workers support Background Sync and Push APIs. Background Sync allows the service worker to defer network requests until connectivity is available. Push allows the server to wake the service worker and trigger actions.

A compromised service worker can use Background Sync to exfiltrate data even after the user navigates away from the site. The data queued for sync persists and is sent when the browser determines connectivity is suitable. This provides a covert exfiltration channel.

Push notifications from a compromised service worker can be used for phishing. The notification appears to come from your legitimate application (because it does -- it comes from your origin's service worker). Users who trust notifications from your application may click through to a phishing page.

Fetch Event Manipulation

The fetch event handler in a service worker intercepts every HTTP request within its scope. A compromised handler can:

Redirect API calls. Change the destination of API requests to an attacker-controlled server that mirrors your API but logs credentials and session tokens.

Modify responses. Add JavaScript to HTML responses, modify JSON payloads to change displayed data (account balances, transaction histories), or inject tracking pixels.

Block requests. Selectively block requests to security-related endpoints (CSP reporting, error logging) to prevent detection.

Serve stale content. Force the application to use outdated cached content instead of fetching current data, which could be exploited in time-sensitive applications (financial trading, security dashboards).

Defensive Measures

Implement strict CSP. Use worker-src to control service worker sources. Combine with script-src to prevent unauthorized script injection that could register malicious workers.

Use Clear-Site-Data header. For authenticated sessions, the Clear-Site-Data: "storage" header on logout responses removes service worker registrations along with other site data. This limits the window of persistence for a compromised worker.

Monitor service worker registrations. Implement client-side monitoring that checks for unexpected service worker registrations. Log registration events to your server for anomaly detection.

Minimize service worker scope. Register workers at the narrowest scope that supports your application's needs. Avoid root-scope registrations unless necessary.

Subresource Integrity for cached assets. When caching assets through the service worker, verify content integrity using hashes. This prevents cache poisoning with modified assets.

How Safeguard.sh Helps

Safeguard.sh monitors the third-party dependencies in your PWA that could serve as vectors for service worker compromise. By tracking every npm package and its transitive dependencies, Safeguard.sh identifies vulnerable or suspicious libraries before they reach production. Its policy enforcement can block builds that include packages with known security issues, reducing the risk that a compromised dependency introduces malicious service worker behavior. For teams building PWAs, Safeguard.sh provides the supply chain layer of defense that complements the browser-level protections.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.