PWA security is very important because PWA can be installed on devices like native apps and can access more device features. Here are the key aspects and best practices for PWA security:
1. Necessity of HTTPS
Why PWA Must Use HTTPS
- Service Worker Requirement: Service Worker can only run in HTTPS environment (localhost excepted)
- Data Security: Protect user data during transmission
- Trust: HTTPS provides identity verification, prevents man-in-the-middle attacks
- Browser Requirements: Modern browsers require PWA to use HTTPS
- Push Notifications: Web Push API requires HTTPS
Configuring HTTPS
nginx# Nginx configuration example server { listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; # SSL configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # HSTS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; } # HTTP redirect to HTTPS server { listen 80; server_name example.com; return 301 https://$server_name$request_uri; }
2. Content Security Policy (CSP)
Role of CSP
CSP helps prevent cross-site scripting (XSS), clickjacking, and other security threats.
Configuring CSP
html<!-- Configure via HTTP header --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"> <!-- Via server configuration -->
nginx# Nginx configuration add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; font-src 'self' https://fonts.gstatic.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';";
CSP Directives Explanation
default-src: Default policyscript-src: Script sourcesstyle-src: Style sourcesimg-src: Image sourcesconnect-src: Network request sourcesfont-src: Font sourcesobject-src: Plugin sourcesframe-ancestors: Allowed parent pages for embeddingform-action: Form submission targets
3. Service Worker Security
Limit Service Worker Scope
javascript// Place Service Worker in root directory to control entire app navigator.serviceWorker.register('/sw.js'); // Or place in specific directory to control only that directory navigator.serviceWorker.register('/app/sw.js', { scope: '/app/' });
Verify Service Worker Updates
javascript// sw.js self.addEventListener('install', event => { event.waitUntil( caches.open('my-cache').then(cache => { return cache.addAll([ '/', '/styles/main.css', '/scripts/app.js' ]); }) ); }); self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { // Only delete caches belonging to current app if (cacheName.startsWith('my-app-')) { return caches.delete(cacheName); } }) ); }) ); });
Prevent Cache Pollution
javascriptself.addEventListener('fetch', event => { const url = new URL(event.request.url); // Only cache same-origin resources if (url.origin !== self.location.origin) { event.respondWith(fetch(event.request)); return; } // Handle same-origin requests event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); });
4. Data Security
Encrypt Sensitive Data
javascript// Use Web Crypto API to encrypt data async function encryptData(data, key) { const encoder = new TextEncoder(); const encodedData = encoder.encode(data); const iv = crypto.getRandomValues(new Uint8Array(12)); const encryptedData = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, key, encodedData ); return { iv: Array.from(iv), data: Array.from(new Uint8Array(encryptedData)) }; } async function decryptData(encryptedData, key) { const iv = new Uint8Array(encryptedData.iv); const data = new Uint8Array(encryptedData.data); const decryptedData = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, key, data ); const decoder = new TextDecoder(); return decoder.decode(decryptedData); }
Secure Storage
javascript// Use IndexedDB to store sensitive data const dbPromise = idb.open('secure-db', 1, upgradeDB => { upgradeDB.createObjectStore('secure-data', { keyPath: 'id' }); }); async function storeSecureData(id, data) { const db = await dbPromise; await db.put('secure-data', { id: id, data: data, timestamp: Date.now() }); } // Avoid storing sensitive information in localStorage // ❌ Not secure localStorage.setItem('token', 'sensitive-token'); // ✅ Use IndexedDB or store encrypted
5. Push Notification Security
VAPID Key Management
javascript// Securely store VAPID keys on server const vapidKeys = { publicKey: process.env.VAPID_PUBLIC_KEY, privateKey: process.env.VAPID_PRIVATE_KEY }; // Don't expose private key on frontend // ❌ Wrong const privateKey = 'my-private-key'; // Don't do this // ✅ Correct: Private key only used on server
Validate Push Subscriptions
javascript// Server-side validate subscription info async function validateSubscription(subscription) { // Check required fields if (!subscription.endpoint || !subscription.keys) { throw new Error('Invalid subscription'); } // Check key format if (!subscription.keys.p256dh || !subscription.keys.auth) { throw new Error('Invalid keys'); } // Validate endpoint format try { new URL(subscription.endpoint); } catch (error) { throw new Error('Invalid endpoint'); } return true; }
6. Cross-Origin Security
CORS Configuration
nginx# Nginx configure CORS location /api/ { add_header 'Access-Control-Allow-Origin' 'https://your-pwa.com'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization'; add_header 'Access-Control-Allow-Credentials' 'true'; if ($request_method = 'OPTIONS') { return 204; } }
javascript// Frontend request configuration fetch('https://api.example.com/data', { method: 'GET', credentials: 'include', // Include cookies headers: { 'Content-Type': 'application/json' } });
7. Authentication and Authorization
Use JWT for Authentication
javascript// Get JWT after login async function login(username, password) { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); // Securely store JWT if (data.token) { await storeSecureToken(data.token); } return data; } // Carry JWT in requests async function fetchData() { const token = await getSecureToken(); const response = await fetch('/api/data', { headers: { 'Authorization': `Bearer ${token}` } }); return response.json(); }
Token Refresh Mechanism
javascript// Auto-refresh expired token async function fetchWithAuth(url, options = {}) { let token = await getSecureToken(); // Check if token is expiring soon if (isTokenExpiringSoon(token)) { token = await refreshToken(); } const response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); // If token is invalid, try to refresh if (response.status === 401) { token = await refreshToken(); return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); } return response; }
8. Security Best Practices
1. Input Validation
javascript// Validate user input function validateInput(input) { // Prevent XSS const sanitized = input.replace(/[<>]/g, ''); // Validate format if (!/^[a-zA-Z0-9]+$/.test(sanitized)) { throw new Error('Invalid input'); } return sanitized; }
2. Prevent CSRF
javascript// Use CSRF token async function submitForm(data) { const csrfToken = await getCsrfToken(); const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(data) }); return response.json(); }
3. Secure Service Worker Updates
javascript// Check for Service Worker updates navigator.serviceWorker.addEventListener('controllerchange', () => { // Notify user that app has been updated showUpdateNotification(); }); // Prompt user to refresh page function showUpdateNotification() { const notification = document.createElement('div'); notification.textContent = 'New version available, click to refresh'; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #007bff; color: white; padding: 15px 20px; border-radius: 4px; cursor: pointer; z-index: 9999; `; notification.addEventListener('click', () => { window.location.reload(); }); document.body.appendChild(notification); }
9. Security Audit and Monitoring
Use Lighthouse for Security Audit
bash# Run Lighthouse security audit lighthouse https://your-pwa.com --view --only-categories=security
Monitor Security Events
javascript// Monitor suspicious activities function logSecurityEvent(event) { fetch('/api/security-log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event: event.type, timestamp: Date.now(), userAgent: navigator.userAgent, url: window.location.href }) }); } // Monitor Service Worker errors navigator.serviceWorker.addEventListener('error', (event) => { logSecurityEvent({ type: 'service-worker-error', error: event.error }); });
Summary
Key points for PWA security:
- Must Use HTTPS: Service Worker and push notifications require HTTPS
- Configure CSP: Prevent XSS and other injection attacks
- Limit Service Worker Scope: Prevent cache pollution
- Securely Store Sensitive Data: Use IndexedDB and encryption
- Validate Push Subscriptions: Prevent malicious pushes
- Properly Configure CORS: Prevent cross-origin attacks
- Use Secure Authentication: JWT + Token refresh
- Input Validation and Output Encoding: Prevent injection attacks
- Regular Security Audits: Use tools like Lighthouse
- Monitor Security Events: Detect and respond to security threats in time