Web Worker 安全攻防:同源策略、CSP 和通信风险
Worker 不是法外之地
很多人以为 Worker 跑在独立线程里,安全性就天然有保障。恰恰相反——Worker 引入了新的攻击面:跨域脚本加载、postMessage 注入、SharedArrayBuffer 竞态,每一个都可能被利用。本文把 Web Worker 相关的安全问题和防御手段讲清楚。
同源策略:第一道防线
Worker 脚本必须和主页面同源(协议 + 域名 + 端口一致)。这是浏览器强制的,不是建议。
javascript// 跨域加载 → 直接报错 new Worker('https://evil.com/worker.js'); // SecurityError // 同源加载 → 正常 new Worker('/workers/task.js');
但同源策略有绕过方式,而这些绕过方式本身就是安全隐患。
Blob URL 的风险
用 Blob URL 可以绕过同源限制,创建内联 Worker:
javascript// 从任意字符串创建 Worker const code = 'self.onmessage = (e) => { /* ... */ }'; const blob = new Blob([code], { type: 'text/javascript' }); new Worker(URL.createObjectURL(blob));
问题在于:如果 code 的内容来自用户输入或外部 API,攻击者就能注入任意代码在 Worker 里执行。永远不要用不受信任的数据构造 Worker 脚本。
用完后必须 URL.revokeObjectURL() 释放,否则内存泄漏。
importScripts 的跨域加载
Worker 内部可以用 importScripts() 加载外部脚本,这个方法不受同源限制:
javascript// worker.js importScripts('https://cdn.example.com/lib.js'); // 允许跨域
这是个设计选择——Worker 需要加载工具库。但这也意味着如果 CDN 被入侵或者 DNS 被劫持,恶意脚本就跑进了你的 Worker。
防御方式:在服务端配置 Content-Security-Policy 的 script-src 指令,限制 importScripts 能加载哪些来源的脚本。
CSP 对 Worker 的约束
Worker 有自己的执行上下文,CSP 的约束方式和主页面不同:
- 同源 Worker 脚本(通过 URL 加载):不受创建它的页面的 CSP 限制
- Blob/data URL Worker:继承创建它的页面的 CSP 策略
- Worker 内的 importScripts:受 Worker 自身的 CSP 约束(如果有)
这意味着如果你想限制 Worker 的行为,需要给 Worker 脚本的 HTTP 响应也加上 CSP 头:
shellContent-Security-Policy: script-src 'self' cdn.example.com
postMessage 通信安全
postMessage 是 Worker 和主线程唯一的通信通道,也是 XSS 注入的潜在入口。
验证消息来源
主线程收到的消息不一定来自你的 Worker。特别是 SharedWorker 和 Service Worker 场景下,多个页面都能发消息:
javascript// 主线程:验证消息来源和格式 worker.onmessage = (e) => { const data = e.data; // 类型校验 if (typeof data !== 'object' || data === null) return; if (typeof data.type !== 'string') return; // 只处理已知的消息类型 const allowedTypes = ['result', 'progress', 'error']; if (!allowedTypes.includes(data.type)) return; // 处理消息 handleMessage(data); }; // Worker 端同理:验证主线程发来的数据 self.onmessage = (e) => { const data = e.data; if (!data || typeof data.type !== 'string') return; // ... };
不要直接执行消息里的代码
javascript// 危险!永远不要这么做 self.onmessage = (e) => { eval(e.data.code); // 任意代码执行 new Function(e.data.fn)(); // 同样危险 };
看似明显,但在模板引擎或动态逻辑场景里容易踩进去。如果必须根据消息执行不同逻辑,用白名单映射:
javascriptconst handlers = { sort: (data) => { /* ... */ }, filter: (data) => { /* ... */ }, }; self.onmessage = (e) => { const handler = handlers[e.data.type]; if (handler) handler(e.data.params); };
SharedArrayBuffer 的安全门槛
SharedArrayBuffer 允许主线程和 Worker 共享同一块内存,没有序列化开销。但它也带来了竞态条件风险——两个线程同时写同一个内存位置,数据就乱了。
浏览器对 SharedArrayBuffer 有严格的安全要求,服务端必须返回以下两个响应头,否则 new SharedArrayBuffer() 直接抛错:
shellCross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
这两个头不是"建议加",而是强制要求。原因是为了防止 Spectre 类的侧信道攻击——没有这些头,恶意页面可以通过 SharedArrayBuffer 读取跨域内存数据。
如果加上 COEP 后你的页面加载第三方资源(图片、脚本)出错了,需要给这些资源的响应加上 Cross-Origin-Resource-Policy: cross-origin 头。
Worker 里能访问什么、不能访问什么
从安全角度看,Worker 的 API 限制本身就是一种防护:
| 能访问 | 不能访问 | 安全意义 |
|---|---|---|
| fetch、WebSocket | document、DOM | 不能直接篡改页面 |
| IndexedDB | localStorage | 避免同步 I/O 竞态 |
| Cache API | window、parent | 隔离全局作用域 |
| Notifications | XMLHttpRequest | 推荐用 fetch 替代 |
| performance | location(只读) | 不能跳转页面 |
这些限制意味着即使 Worker 代码被攻破,攻击者也无法直接操作 DOM 或窃取 localStorage 中的 token。Worker 的攻击半径被刻意缩小了。
实际攻击场景
场景 1:CDN 供应链攻击。你的 Worker 用 importScripts('https://cdn.example.com/lib.js'),CDN 被入侵后恶意代码跑进了 Worker。防御:CSP 限制 script-src,或改用 npm 包 + 打包工具。
场景 2:postMessage 中间人。攻击者在页面注入脚本拦截 Worker 通信,篡改消息内容。防御:消息加签名校验,关键字段用加密传输。
场景 3:Blob Worker 代码注入。从服务端获取的配置数据直接拼进 Worker 代码字符串,攻击者通过配置接口注入恶意代码。防御:Worker 代码和数据严格分离,用 postMessage 传配置,不拼字符串。
安全检查清单
- Worker 脚本是否只从同源加载?如果是 Blob URL,代码来源是否可信?
importScripts加载的外部脚本是否有 CSP 保护?- postMessage 通信是否做了类型校验和白名单过滤?
- 有没有用
eval或new Function执行消息中的代码? - SharedArrayBuffer 是否配了 COOP/COEP 响应头?
- Worker 脚本 MIME 类型是否为
text/javascript? - Blob URL 用完后是否调用了
revokeObjectURL?