5月28日 02:18

Service Worker 有哪些安全风险和防护手段?

为什么 Service Worker 天生需要安全约束

Service Worker 本质是一个浏览器级的网络代理——它能拦截页面发出的所有请求、读写 Cache Storage、接收推送消息。这意味着一旦攻击者控制了 Service Worker,就能在用户毫无感知的情况下窃取数据、注入恶意内容。浏览器因此对 Service Worker 施加了严格的安全限制,而理解这些限制背后的原因,是回答本题的关键。

HTTPS 是硬性前提

Service Worker 只能在 HTTPS 环境下注册(localhost 例外)。这不是建议,是强制要求。

原因很直接:HTTP 明文传输,中间人可以篡改响应内容,将恶意脚本注入 sw.js,从而在用户浏览器中植入一个持久的代理。由于 Service Worker 注册后即使关闭页面也继续运行,这种攻击的持久性和隐蔽性远超普通 XSS。

javascript
// 注册前检查安全上下文 if (window.isSecureContext) { navigator.serviceWorker.register('/sw.js'); } else { console.error('Service Worker 需要 HTTPS 环境'); }

值得注意的细节:localhost 被视为安全上下文仅限开发阶段,生产环境绝不能依赖此例外。

作用域限制与越权防护

Service Worker 默认只能控制其脚本所在目录及其子路径下的页面。注册时可通过 scope 参数缩小范围,但不能扩大:

javascript
// scope 只能缩小,不能超出脚本所在目录 navigator.serviceWorker.register('/sw.js', { scope: '/app/' }); // /app/page.html → 可控制 // /other/page.html → 无法控制

浏览器通过 Scope 限制阻止一个 Service Worker 越权接管其他路径的请求。如果需要更大作用域,必须将脚本文件放在更高层级的目录,而非通过参数绕过。

fetch 事件中处理请求时,必须校验路径,防止路径遍历攻击:

javascript
self.addEventListener('fetch', event => { const { pathname } = new URL(event.request.url); if (pathname.includes('..') || pathname.includes('//')) { event.respondWith(new Response('Invalid path', { status: 400 })); return; } const allowed = ['/api/', '/assets/', '/static/']; if (!allowed.some(p => pathname.startsWith(p))) { event.respondWith(new Response('Forbidden', { status: 403 })); return; } event.respondWith(caches.match(event.request)); });

缓存中的敏感数据泄露

这是面试中容易被追问的高频点。Service Worker 拥有 Cache Storage 的读写权限,如果盲目缓存所有响应,用户认证信息、个人数据都会被持久化到磁盘,其他同源脚本可以读取这些缓存。

核心原则:敏感数据永远不进缓存

javascript
const PRIVATE_PATHS = ['/api/auth/', '/api/user/', '/api/payment/']; self.addEventListener('fetch', event => { const { pathname } = new URL(event.request.url); const isPrivate = PRIVATE_PATHS.some(p => pathname.startsWith(p)); if (isPrivate) { // 敏感请求只走网络,不缓存 event.respondWith(fetch(event.request)); return; } // 公共资源走缓存策略 event.respondWith( caches.match(event.request).then(r => r || fetch(event.request)) ); });

清理旧缓存时也要注意命名空间,避免误删其他应用的缓存:

javascript
self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(names => Promise.all( names.filter(n => n.startsWith('my-app-') && n !== CACHE_NAME) .map(n => caches.delete(n)) ) ) ); });

CSP 与 Service Worker 的交互

页面设置的 CSP 对 Service Worker 脚本本身不直接生效(SW 脚本由浏览器单独加载),但 CSP 会限制页面中注册 Service Worker 的方式——内联脚本创建的 Blob URL 注册会被 CSP 阻止:

javascript
// 这种方式会被 CSP 拦截 const blob = new Blob([swCode], { type: 'application/javascript' }); navigator.serviceWorker.register(URL.createObjectURL(blob)); // ❌ // 只能用标准的外部脚本注册 navigator.serviceWorker.register('/sw.js'); // ✅

另一个关键点:Service Worker 内部通过 importScripts() 加载的脚本不受页面 CSP 约束,但受 Service Worker 自身响应头中 Content-Security-Policy 的约束。服务器应在 SW 脚本响应头中设置 CSP,限制 importScripts 可加载的来源。

XSS 向 Service Worker 的渗透路径

虽然 Service Worker 没有 DOM 访问权限,但它能监听 message 事件,攻击者可利用页面中的 XSS 漏洞向 SW 发送恶意指令:

javascript
// SW 端:不验证消息来源的写法是危险的 self.addEventListener('message', event => { eval(event.data); // 严重漏洞 }); // 安全写法:验证 origin + 白名单校验 action self.addEventListener('message', event => { if (event.origin !== 'https://your-domain.com') return; const { action } = event.data; const ALLOWED = ['skipWaiting', 'claimClients']; if (!ALLOWED.includes(action)) return; action === 'skipWaiting' && self.skipWaiting(); action === 'claimClients' && self.clients.claim(); });

这条渗透路径意味着:页面 XSS 的危害会因为 Service Worker 的存在而放大——攻击者不仅能操作当前页面,还能指挥后台代理篡改后续所有请求。

更新机制中的安全风险

Service Worker 的更新依赖浏览器对 sw.js 的字节级比对。如果攻击者能控制服务器响应(如 CDN 被入侵),就可以推送恶意更新。

防护手段:

  • 在 SW 脚本响应头设置 Cache-Control: no-cache,确保浏览器每次都检查更新
  • 使用 SRI(Subresource Integrity)验证脚本完整性:<script src="/sw.js" integrity="sha384-xxxxx">(注意:register() 方式不支持 SRI,需配合 Service Worker 的 importScripts 做完整性校验)
  • 实现紧急 kill-switch:部署一个功能为 self.unregister() 的新版 SW,用于紧急卸载
javascript
// kill-switch: 紧急卸载 self.addEventListener('activate', () => { self.registration.unregister(); });

中间人攻击的纵深防御

虽然 HTTPS 是第一道防线,但 Service Worker 还可以补充检查响应头,作为纵深防御:

javascript
self.addEventListener('fetch', event => { event.respondWith( fetch(event.request).then(response => { // 检查关键安全头是否存在 const hsts = response.headers.get('Strict-Transport-Security'); if (!hsts) { console.warn('缺少 HSTS 头,可能存在降级攻击风险'); } return response; }) ); });

不过要注意,这种检查本身也在 SW 中运行——如果 SW 已被篡改,检查也就失效了。因此 HTTPS + 证书体系才是根本,SW 检查只是辅助告警。

追问方向

  • Service Worker 被恶意注册后如何彻底清除?registration.unregister() + Clear-Site-Data 响应头可强制清除所有缓存和注册
  • 第三方 iframe 中的 Service Worker 会影响宿主页面吗? → Chrome 已将第三方 iframe 的 SW 按顶级站点分区(partitioned),与宿主页面的 SW 隔离
  • 如何防止旧版 Service Worker 拒绝更新?skipWaiting() 强制激活新版,但需确保旧缓存清理逻辑在 activate 事件中执行
  • Service Worker 和 Web Worker 的安全模型有何差异? → Web Worker 受同源策略约束但无法拦截网络请求,Service Worker 的代理能力是其额外安全风险的根源
标签:Service Worker