Join our Newsletter — 33% off our NHI Course
Home› Guides› Progressive Web App Security: Service Workers, Caches, Manifests…
Guide Web & API Security

Progressive Web App Security: Service Workers, Caches, Manifests and Push Done Safely

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 8 November 2026 17 min read
On this page

A progressive web app installs a programmable proxy, the service worker, between your users and your origin, and that proxy keeps running and keeps serving cached content long after the page that registered it has closed. That is what makes offline use, fast start-up and push notifications possible, and it is also why a single flaw can outlive a fix on the server. This guide covers the parts of a PWA that ordinary web security reviews tend to miss: service worker registration and scope, the update lifecycle, the Cache API, the web app manifest, push subscriptions and the kill switch you need when something goes wrong. It is written for web developers, application security engineers and penetration testers who already know the browser basics. For the wider picture of scripts running in users' browsers, see the Client-Side Security Guide.

Key takeaways

  • A service worker is a persistent, origin-wide man in the middle. Its fetch handler sees every request from controlled pages, including cross-origin sub-resource requests, and can answer any of them with a response it builds itself.
  • Scope is your blast radius. Register the worker at the narrowest path that works, never widen scope with the Service-Worker-Allowed header without review, and treat any user-controlled file served from your origin as a potential worker script.
  • Caching rules decide what leaks and what goes stale. Never cache authenticated or personalised responses in a shared cache name, version every cache, and delete old caches in the activate handler.
  • Plan the kill switch before launch. A no-op worker at the same URL and a Clear-Site-Data "storage" response from logout or an incident endpoint are what let you remove a bad worker from devices you cannot touch.
  • Push subscriptions are credentials. The subscription endpoint is a capability URL and the VAPID private key signs every message, so both need the handling you give API keys.

How a PWA changes the threat model

A PWA is still a website: the same origin, the same cookies, the same HTML and JavaScript. What changes is persistence and interception. Three browser features do most of the work, and each moves a security property.

ComponentWhat it doesSecurity property it changesMain risk
Service workerEvent-driven script that intercepts fetch, push and sync events for pages in its scopeIntegrity of every in-scope response; persistence after the tab closesA malicious or buggy worker controls traffic until it is replaced or unregistered
Cache API (caches)Origin-scoped store of request and response pairs that the worker reads and writesConfidentiality of cached responses; freshness of codePersonal data left on shared devices; stale vulnerable JavaScript served from cache
Web app manifestJSON file describing name, icons, start_url, scope, id and display modeHow the app is presented when installed, often without a visible address barSpoofed look and feel; identifiers placed in start_url
Push API and notificationsLets your server wake the worker and show notifications through a browser push serviceA new server-to-device channel authenticated by keysLeaked endpoints or VAPID keys allow unwanted or abusive notifications

MDN's service worker documentation is explicit that a fetch event fires for every resource request in scope, including a cross-origin image embedded by an in-scope page, and that the handler can supply any response through respondWith(). A compromised worker therefore has the same reach as a compromised reverse proxy, but runs on the user's device where your server logs cannot see it. In ATT&CK terms the closest fits are T1189 Drive-by Compromise for the initial script injection and T1185 Browser Session Hijacking for what a hostile worker can do with an authenticated session.

Registration and scope: keep the blast radius small

The browser enforces several rules at registration, and they are your first line of defence. Your job is to avoid weakening them.

  • Secure context only: the ServiceWorker, ServiceWorkerRegistration and ServiceWorkerContainer interfaces are marked [SecureContext] in the W3C specification, so registration fails over plain HTTP. Browsers treat localhost as secure to help development, which is a reason to keep development builds off shared hostnames.
  • Same-origin script: the worker script must come from the registering page's origin, and the worker runs in that origin. A third-party CDN cannot host your worker directly.
  • Default scope is the script's directory: a worker at /js/sw.js controls only URLs under /js/ unless the server sends a Service-Worker-Allowed header that permits a broader path. Registration with a scope broader than the script path allows fails.
  • Several registrations per origin: the specification allows many registrations at one origin as long as the scope URLs differ, and registering an identical scope replaces the existing registration. That replacement rule is useful for recovery and dangerous if an attacker can register first.
  • Trusted Types: the specification passes the script URL through Trusted Types before parsing, so a Trusted Types policy can restrict which URLs are allowed to become workers.

Why same-origin is not enough

The same-origin rule means an attacker cannot register a worker from their own domain. It does not stop them using any endpoint on your origin that returns attacker-influenced JavaScript, for example a user upload served from the same host, a JSONP callback parameter, or an open path that reflects input with a script-compatible content type. If such an endpoint sits at the root, a single cross-site scripting flaw can register a root-scoped worker that survives after the XSS is patched. Controls that close this gap:

  1. Serve user uploads from a separate, cookieless origin, never from the application origin.
  2. Set a Content-Security-Policy that includes a worker-src directive listing only your worker script path, so injected code cannot register a different script.
  3. Return X-Content-Type-Options: nosniff and correct Content-Type values so data endpoints cannot be loaded as scripts.
  4. Strip or reject a Service-Worker-Allowed header anywhere it is not deliberately set, and alert on it in configuration review.

See the Content Security Policy Guide and the Cross-Site Scripting (XSS) Prevention Guide for the general controls.

The update lifecycle and why fixes can be slow

Server-side fixes take effect on the next request. Service worker fixes take effect only when the browser installs and activates the new worker, and the lifecycle deliberately delays that.

StageWhat happensSecurity implication
Update checkThe browser checks for a new script on navigation and functional events; a registration counts as stale when the last check was more than 86,400 seconds (24 hours) agoA user who keeps a tab open can run old code; the 24 hour rule bounds how long the HTTP cache can delay discovery
installNew worker downloads and pre-caches assets; if the waitUntil() promise rejects, installation failsA failed install leaves the old, possibly vulnerable, worker in control
waitingNew worker waits while the old version still controls open pagesSecurity fixes sit idle until every controlled tab closes
activateNew worker takes over and should delete old cachesOld caches left behind keep stale code and data
skipWaiting() and clients.claim()Force immediate activation and take control of already open pagesUse for security releases; test for pages running mixed versions
  • updateViaCache: the registration option accepts imports (the default), all or none. Set none so update checks for the worker and its imported scripts bypass the HTTP cache, and serve sw.js with Cache-Control: no-cache.
  • Imported scripts: code pulled in through importScripts() runs with the worker's full privileges. Host it on your origin, version it in the file name and avoid importing third-party scripts into the worker at all.
  • Global state: MDN warns that one worker serves many pages and global variables are shared, so never hold per-user secrets in worker globals.

Cache API: what to cache and what never to cache

The Cache API is a persistent, origin-wide store. MDN notes that cache contents are kept until you clear them, so anything you put there outlives the session unless your code removes it. Most PWA data exposure comes from caching strategies copied from tutorials that cache every response.

ContentRecommended strategyWhy
Versioned static assets (hashed JS, CSS, fonts)Cache first, pre-cached at install, cache name tied to releaseContent is immutable for that version; hash in the name prevents mixing releases
App shell HTMLNetwork first with cached fallbackGets fixes quickly while still working offline
Public API dataStale while revalidate with short lifetimeAcceptable to show slightly old data
Authenticated or personalised API responsesNetwork only, or a per-user cache cleared at logoutA shared cache can serve one user's data to the next user of a device
Responses marked Cache-Control: no-storeNever cache; respect the header in your fetch handlerThe Cache API does not apply HTTP caching rules for you
Error, redirect and opaque responsesDo not cache; check response.ok before cache.put()Caching a 500 page or a login redirect breaks the app offline and can pin an attacker-influenced response

Cache poisoning inside the worker

Cache poisoning in a PWA usually means the worker stores a response it should not have trusted, then serves it repeatedly. Typical routes are caching cross-origin responses from a CDN without integrity checks, keying cache entries on URLs that include attacker-controlled query parameters, or caching a response delivered while the user was on a hostile network before HSTS was in place. Defences are to cache only same-origin or integrity-verified assets, normalise cache keys to an allow list of paths, apply HSTS before the first visit where possible, and rotate cache names on every release. OWASP ASVS 5.0 requirement V3.6.1 asks that externally hosted assets be static, versioned and protected with Subresource Integrity; apply the same rule to anything the worker pre-caches. See the Subresource Integrity (SRI) Guide.

Sensitive data and shared devices

ASVS 5.0 V14.3.1 requires authenticated data to be cleared from client storage when the session ends, and names the Clear-Site-Data header as a help; V14.3.2 requires Cache-Control: no-store on sensitive responses; V14.3.3 limits what may sit in browser storage. In a PWA that means your logout flow must delete per-user caches and IndexedDB stores as well as cookies. The Browser Storage Security Guide covers token placement.

The web app manifest and installed app presentation

The manifest controls how an installed PWA looks and launches. The W3C specification is a Working Draft (6 November 2026 at the time of checking) and describes itself as not stable, so treat the details below as current behaviour that may change.

  • start_url: must be same origin as the document; otherwise the browser falls back to the document URL. Do not put user identifiers in it: the specification's privacy section notes that data encoded in start_url is not cleared when a user clears site data.
  • scope: navigation outside scope drops the app back to a normal browser view. The specification recommends always declaring scope, preferably "/", because users can install from any page. Security-wise, keep login and payment flows inside scope so users are not bounced to a view that looks different mid-flow.
  • id: the application's identity, used to decide whether a manifest is a new app or an update. It must be same origin as start_url. Changing it creates a different installed app.
  • name, short_name and icons: the specification lists these as security-sensitive; user agents should present changes to the user and get express permission before applying them. Expect users to see a prompt if you rebrand.
  • display: standalone and fullscreen hide the address bar, so users cannot check the origin. Never ask for credentials in a view that could be mistaken for another app, and give users a visible way to see where they are.
  • Cross-origin manifests: a manifest on a different origin must be fetched with CORS, and the credentials mode follows the link element's crossorigin attribute. Host the manifest on your own origin unless you have a reason not to.

Push notifications and their keys

Web Push has three keys or secrets, and each needs an owner. The application server identifies itself to the browser vendor's push service with VAPID (RFC 8292): it signs a JWT with an ECDSA P-256 key using ES256 and sends it in an Authorization: vapid t=..., k=... header. The JWT carries an aud claim with the push service origin, an exp that must not be more than 24 hours from the request, and an optional sub contact URI.

SecretWhere it livesIf it leaksControl
VAPID private keyYour push sending serviceAnyone can send pushes to subscriptions restricted to your keyStore in a secrets manager or HSM, restrict which workloads can sign, plan rotation (RFC 8292 advises gradual migration because new keys break linkability)
Subscription endpoint URLBrowser, then your subscription databaseMDN describes it as a capability URL: anyone who has it can target that userEncrypt the database column, never log full endpoints, delete on unsubscribe or 404/410 responses
Subscription p256dh and auth valuesBrowser, then your subscription databaseWith the endpoint, allows sending readable messages to that userTreat the whole subscription object as a credential tied to the user record
Signed VAPID JWTIn transit to the push serviceRFC 8292 notes a stolen token can be replayed until it expiresShort expiry, HTTPS only
  • CSRF on subscription registration: MDN stresses protecting the request that sends the subscription to your server against cross-site request forgery; otherwise an attacker can bind their own subscription to a victim's account.
  • Restricted subscriptions: always pass applicationServerKey when subscribing, so the push service only accepts messages signed with your key.
  • Content discipline: notifications appear on lock screens. Do not put one-time codes, balances or health details in the notification body; send a prompt to open the app instead.
  • Permission prompts: request notification permission after a user action that explains the benefit, never on first load. Abusive prompting is how push becomes a spam and phishing channel.

Where identity fits

Two identities matter in a PWA. The user's session usually lives in cookies or tokens that the worker can see in every request it proxies, so an attacker who controls the worker can ride the session without stealing it; short session lifetimes and server-side revocation limit the damage. The second is a non-human identity: the push sending service holds the VAPID private key and often API credentials to your subscription store. Inventory it, give it its own workload identity rather than a shared application secret, and rotate it on staff changes. See the Session Hijacking and Cookie Theft Guide.

Implementation: a phased approach

  1. Inventory: list every origin and path that registers a worker, every cache name in use and every service that sends push. Use the browser developer tools' Application panel on each environment to see registrations and caches.
  2. Build the kill switch first: prepare a minimal worker that installs, calls skipWaiting(), deletes all caches and unregisters itself, deployable at the same script URL. Add a server route that returns Clear-Site-Data: "cache", "storage"; MDN documents that the "storage" directive unregisters service workers for the origin and that the header only works in secure contexts.
  3. Harden registration: narrow scope, add worker-src to CSP, move uploads to a separate origin, set updateViaCache: 'none' and Cache-Control: no-cache on the worker script.
  4. Rewrite caching rules: classify routes using the table above, add response.ok and same-origin checks before caching, and add per-user cache clearing to logout.
  5. Pilot: release to a small cohort or internal users, confirm the new worker activates, old caches are deleted and logout leaves no personal data in Cache Storage or IndexedDB.
  6. Test the rollback: deploy the kill switch in staging, confirm it removes the worker from a device that was offline during the release, then restore the normal worker.
  7. Add to the SDLC: include the worker script, manifest and push service in code review, dependency scanning and penetration test scope.

Small and large teams

A small team gets most of the value from three things: narrow scope, no caching of authenticated responses and a tested kill switch. Larger organisations with several apps on one origin should give each app its own path-scoped worker and cache prefix, centralise VAPID keys in a secrets manager, and add worker and manifest changes to change approval.

Detection and measurement

A worker runs on the client, so you need signals you can collect server-side or from the page.

  • Worker version beacon: have the worker add a version header to its own requests or post its version on activation, and alert when unexpected versions appear.
  • CSP reports: ASVS V3.4.7 asks for a CSP report location; worker-src violations indicate an attempt to register an unapproved script.
  • Header monitoring: alert on any response carrying Service-Worker-Allowed outside approved paths.
  • Push service responses: rising 401 or 403 responses from push services indicate key problems; 404 or 410 means stale subscriptions to delete.
MetricTarget
Share of active clients on the latest worker version 72 hours after a security releaseAbove 95 percent
Routes returning authenticated data that are cached by the workerZero
Time to deploy and confirm the kill switch in an exerciseUnder one hour
Worker or manifest changes merged without security reviewZero
Age of the VAPID key against your rotation policyWithin policy

Framework mapping

Control areaNIST SP 800-53 Rev. 5NIST CSF 2.0OWASP ASVS 5.0
Worker and imported script integritySI-7, SC-18PR.PS-06V3.4.3, V3.6.1
Cached and stored data protectionSC-28PR.DS-10V14.3.1, V14.3.2, V14.3.3
Transport and push channelSC-8, SC-12PR.DS-02V3.4.3
Inventory of workers and push servicesSA-11ID.AM-02Not specific
Testing worker behaviourSA-11ID.RA-01Not specific

Common failure patterns

  • Cache everything: a tutorial fetch handler that calls cache.put() on every response, so account pages and API data sit in Cache Storage for the next user of a shared laptop.
  • Uploads on the app origin: a file upload feature serving content from the same origin, which lets an XSS register a root-scoped worker from an uploaded file.
  • No way back: a broken worker shipped with no kill switch, leaving users on a cached broken app until they clear site data by hand.
  • Old caches never deleted: new releases add cache names but the activate handler does not remove old ones, so vulnerable JavaScript remains on disk and can be served again by a later bug.
  • Logging push subscriptions: full endpoint URLs written to application logs or analytics, turning a capability URL into something many staff and tools can read.
  • VAPID key in the front end repository: the private key committed alongside the public one, which is a secrets leak like any other. See the Secrets Management Guide.

Practitioner checklist

  • Register each worker at the narrowest workable scope and review any Service-Worker-Allowed header.
  • Add worker-src to CSP and serve user uploads from a separate origin.
  • Set updateViaCache: 'none', serve the worker with Cache-Control: no-cache and use skipWaiting() for security releases.
  • Never cache authenticated, personalised, error or no-store responses in shared caches.
  • Version cache names per release and delete old caches in the activate handler.
  • Clear per-user caches, IndexedDB and cookies at logout, using Clear-Site-Data where it fits.
  • Keep the manifest on your origin, declare scope and id, and keep identifiers out of start_url.
  • Protect subscription registration against CSRF, treat subscriptions as credentials and never log endpoints.
  • Store the VAPID private key in a secrets manager and document its rotation.
  • Build, exercise and document a kill switch before going live.

Standards and references

Related NHI Mgmt Group resources: Client-Side Security Guide · Content Security Policy Guide · Browser Storage Security Guide · Same-Origin Policy and Browser Security Model Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 8 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org