Service Worker 安全注意事项详解
Service Worker 作为浏览器后台运行的代理服务器,具有强大的能力,同时也带来了一些安全风险。了解这些安全问题对于开发安全的 Web 应用至关重要。
1. HTTPS 要求
为什么必须使用 HTTPS
javascript// Service Worker 只能在 HTTPS 环境下注册 // 例外:localhost 允许 HTTP if ('serviceWorker' in navigator) { // 检查是否是安全上下文 if (window.isSecureContext) { navigator.serviceWorker.register('/sw.js'); } else { console.error('Service Worker 需要 HTTPS 环境'); } }
安全风险:
- HTTP 环境下 Service Worker 可能被中间人攻击篡改
- 攻击者可注入恶意 Service Worker 拦截所有网络请求
- 用户的敏感数据可能被窃取
检测安全上下文
javascript// 检查当前环境是否安全 function checkSecureContext() { if (!window.isSecureContext) { console.warn('当前不是安全上下文,Service Worker 功能受限'); return false; } return true; } // 或者检查协议 function isSecureProtocol() { return location.protocol === 'https:' || location.hostname === 'localhost'; }
2. 作用域限制
作用域安全
javascript// Service Worker 只能控制其作用域内的页面 // 注册时指定作用域 navigator.serviceWorker.register('/sw.js', { scope: '/app/' // 只能控制 /app/ 下的页面 }); // 尝试访问作用域外的资源会失败 // /app/page.html ✅ 可以控制 // /other/page.html ❌ 无法控制
路径遍历防护
javascript// ❌ 危险:不验证路径可能导致安全问题 self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 恶意请求可能包含 ../ 等路径遍历字符 caches.match(url.pathname); // 危险! }); // ✅ 安全:验证和清理路径 self.addEventListener('fetch', event => { const url = new URL(event.request.url); const pathname = url.pathname; // 验证路径不包含遍历字符 if (pathname.includes('..') || pathname.includes('//')) { event.respondWith(new Response('Invalid path', { status: 400 })); return; } // 只允许访问白名单路径 const allowedPaths = ['/api/', '/assets/', '/static/']; const isAllowed = allowedPaths.some(path => pathname.startsWith(path)); if (!isAllowed) { event.respondWith(new Response('Forbidden', { status: 403 })); return; } event.respondWith(caches.match(event.request)); });
3. 内容安全策略(CSP)
CSP 对 Service Worker 的影响
javascript// 设置 CSP 防止 XSS 攻击 // 在 HTTP 响应头中设置: // Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' // Service Worker 脚本本身需要符合 CSP // 内联脚本可能被阻止
安全的 Service Worker 脚本加载
javascript// ✅ 推荐:加载外部脚本 navigator.serviceWorker.register('/sw.js'); // ❌ 避免:使用内联 Service Worker const swCode = ` self.addEventListener('fetch', ...); `; const blob = new Blob([swCode], { type: 'application/javascript' }); const url = URL.createObjectURL(blob); navigator.serviceWorker.register(url); // 可能违反 CSP
4. 缓存安全
敏感数据缓存
javascript// ❌ 危险:缓存敏感信息 self.addEventListener('fetch', event => { if (event.request.url.includes('/api/user/profile')) { event.respondWith( fetch(event.request).then(response => { // 缓存包含个人信息的响应 caches.open('api-cache').then(cache => { cache.put(event.request, response.clone()); // 危险! }); return response; }) ); } }); // ✅ 安全:不缓存敏感数据 const SENSITIVE_PATHS = [ '/api/auth/', '/api/user/profile', '/api/payment/' ]; self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 检查是否是敏感路径 const isSensitive = SENSITIVE_PATHS.some(path => url.pathname.includes(path) ); if (isSensitive) { // 敏感请求直接走网络,不缓存 event.respondWith(fetch(event.request)); return; } // 非敏感请求可以使用缓存 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); });
缓存清理安全
javascript// ✅ 安全的缓存清理 self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { // 只删除当前 Service Worker 创建的缓存 // 避免删除其他应用的缓存 if (cacheName.startsWith('my-app-')) { return caches.delete(cacheName); } }) ); }) ); });
5. 跨站脚本攻击(XSS)防护
防止恶意脚本注入
javascript// ❌ 危险:直接使用用户输入 self.addEventListener('message', event => { const userInput = event.data.message; // 直接执行用户输入可能导致 XSS eval(userInput); // 极度危险! }); // ✅ 安全:验证和清理输入 self.addEventListener('message', event => { const data = event.data; // 验证消息来源 if (event.origin !== 'https://trusted-domain.com') { console.error('Untrusted origin:', event.origin); return; } // 验证数据类型 if (typeof data.action !== 'string') { return; } // 使用白名单验证操作 const allowedActions = ['skipWaiting', 'claimClients']; if (!allowedActions.includes(data.action)) { console.error('Invalid action:', data.action); return; } // 安全执行 switch (data.action) { case 'skipWaiting': self.skipWaiting(); break; case 'claimClients': self.clients.claim(); break; } });
6. 中间人攻击防护
证书固定(Certificate Pinning)
javascript// 虽然 Service Worker 无法直接实现证书固定 // 但可以通过检查响应头来增强安全性 self.addEventListener('fetch', event => { event.respondWith( fetch(event.request).then(response => { // 检查安全响应头 const headers = response.headers; // 检查 HSTS 头 if (!headers.get('Strict-Transport-Security')) { console.warn('Missing HSTS header'); } // 检查 CSP 头 if (!headers.get('Content-Security-Policy')) { console.warn('Missing CSP header'); } return response; }) ); });
7. 权限控制
最小权限原则
javascript// ✅ 只请求必要的权限 // 推送通知权限 async function requestPushPermission() { const permission = await Notification.requestPermission(); return permission === 'granted'; } // 后台同步权限 async function requestSyncPermission() { const registration = await navigator.serviceWorker.ready; if ('sync' in registration) { // 检查权限状态 const status = await navigator.permissions.query({ name: 'periodic-background-sync' }); if (status.state === 'granted') { return true; } } return false; }
8. 更新安全
安全的更新机制
javascript// ✅ 验证 Service Worker 更新 navigator.serviceWorker.register('/sw.js').then(registration => { registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed') { // 验证新版本的完整性 // 可以通过计算哈希值来验证 verifyServiceWorkerIntegrity(newWorker).then(isValid => { if (isValid) { // 提示用户更新 showUpdateNotification(newWorker); } else { console.error('Service Worker 完整性验证失败'); } }); } }); }); }); // 验证 Service Worker 完整性(示例) async function verifyServiceWorkerIntegrity(worker) { try { const response = await fetch('/sw.js'); const scriptContent = await response.text(); // 这里可以添加哈希验证逻辑 // 比如与服务器返回的哈希值对比 return true; } catch (error) { console.error('验证失败:', error); return false; } }
9. 数据泄露防护
防止缓存泄露
javascript// ✅ 安全的缓存策略 const PUBLIC_RESOURCES = [ '/', '/index.html', '/styles.css', '/app.js', '/images/' ]; const PRIVATE_RESOURCES = [ '/api/user/', '/api/orders/', '/dashboard/' ]; self.addEventListener('fetch', event => { const url = new URL(event.request.url); // 检查是否是公共资源 const isPublic = PUBLIC_RESOURCES.some(path => url.pathname.startsWith(path) ); // 检查是否是私有资源 const isPrivate = PRIVATE_RESOURCES.some(path => url.pathname.startsWith(path) ); if (isPrivate) { // 私有资源:网络优先,不缓存 event.respondWith( fetch(event.request).catch(() => { return new Response('Network error', { status: 503 }); }) ); } else if (isPublic) { // 公共资源:可以使用缓存 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); } else { // 未知资源:默认网络请求 event.respondWith(fetch(event.request)); } });
10. 安全最佳实践清单
开发阶段
- 确保所有环境使用 HTTPS(除 localhost)
- 合理设置 Service Worker 作用域
- 不缓存敏感数据(用户信息、支付数据等)
- 验证所有用户输入
- 实施 CSP 策略
- 定期更新 Service Worker
生产环境
- 监控 Service Worker 异常行为
- 实施 Subresource Integrity (SRI)
- 配置 HSTS 头
- 定期审计缓存内容
- 实施访问控制
- 记录安全日志
安全测试建议
javascript// 安全测试清单 const securityTests = { // 1. 检查 HTTPS checkHTTPS: () => location.protocol === 'https:', // 2. 检查作用域 checkScope: async () => { const registration = await navigator.serviceWorker.ready; console.log('Service Worker scope:', registration.scope); return registration.scope; }, // 3. 检查缓存内容 checkCacheContents: async () => { const cacheNames = await caches.keys(); for (const name of cacheNames) { const cache = await caches.open(name); const requests = await cache.keys(); console.log(`Cache "${name}" contains ${requests.length} items`); } }, // 4. 检查权限 checkPermissions: async () => { const permissions = await navigator.permissions.query({ name: 'notifications' }); console.log('Notification permission:', permissions.state); } };
总结
Service Worker 安全要点:
- 必须使用 HTTPS:防止中间人攻击
- 合理设置作用域:限制控制能力
- 不缓存敏感数据:防止数据泄露
- 验证用户输入:防止 XSS 攻击
- 实施 CSP:增强内容安全
- 定期更新:修复安全漏洞
- 最小权限:只请求必要权限