Electron 安全纵深:contextIsolation、最小暴露面与 CSP 防线
Electron 应用同时拥有 Chromium 的完整 Web 能力和 Node.js 的完整系统权限------这意味着一个 XSS 就等于任意代码执行:读取用户全部文件、调用子进程、持久化驻留。安全不是发版前的一次检查,而是贯穿架构的纵深防线。本文拆解 Electron 安全模型的五层防御:渲染器加固、桥接最小化、导航控制、内容安全策略与文件系统边界。
一、威胁模型:先认清攻击者从哪进来
视频编辑器看似纯本地工具,攻击面却不少:
- 导入即触发 :用户导入第三方字幕(SRT/ASS)、项目文件、素材元数据------这些都是攻击者可控文本,会被渲染到 UI。
- AI 内容回流:LLM 转写、TTS、分析结果含攻击者控制的文本(恶意视频里的 ASR 结果就是攻击载体)。
- 远程内容:更新公告、模板市场、帮助页如果加载远程网页,供应链风险进入。
- 供应链:npm 依赖投毒、安装包被篡改。
核心原则一句话:渲染进程里的一切内容都可能是恶意的,包括"我们自己生成的"内容。ASR 文本来自视频文件,视频文件来自任何人。
二、第一层:渲染器加固------三个开关没有妥协空间
ts
new BrowserWindow({
webPreferences: {
contextIsolation: true, // ① 上下文隔离,必须开
nodeIntegration: false, // ② 渲染进程禁用 Node,必须关
sandbox: true, // ③ 沙箱,能开尽开
webSecurity: true, // 永远不要为了"跨域方便"关掉
allowRunningInsecureContent: false,
preload: path.join(__dirname, 'preload.js'),
},
})
contextIsolation 的真实含义
隔离后,渲染页面的 JS 世界与 preload 脚本的世界是两个独立的 Realm :preload 里的 require('electron')、Node API,页面里的任何脚本(包括被 XSS 注入的)都摸不到。preload 通过 contextBridge.exposeInMainWorld 显式递过去的东西,是渲染侧能拿到的全部能力。
没有隔离时,XSS 注入的脚本可以直接 window.require('child_process')------这就是 RCE。隔离后它只能看到你递过去的 API 表面。
nodeIntegration 关闭后的常见误区
"关了 nodeIntegration 就安全了"是错的------在 contextIsolation 出现前,preload 与页面共享 window,preload 里 window.something = require(...) 等于白关。两道防线必须同时存在:nodeIntegration:false 挡住直接 require,contextIsolation:true 保证 preload 不泄漏。
sandbox 的取舍
沙箱开启后 preload 只能用 polyfill 的少量 API(不能直接 require 大部分 Node 模块),部分老依赖不兼容。现代 Electron 的沙箱 preload 仍可 require('electron') 的有限子集(ipcRenderer、contextBridge),对干净架构足够。能开就开;不能开的窗口要书面登记原因并额外加固。
三、第二层:preload 最小暴露面
preload 是唯一的桥,桥的宽窄决定攻击面。反面教材与正解:
ts
// ❌ 反模式:把整个 ipcRenderer 递出去,等于任意通道收发
contextBridge.exposeInMainWorld('api', {
send: (channel: string, data: unknown) => ipcRenderer.send(channel, data),
invoke: (channel: string, data: unknown) => ipcRenderer.invoke(channel, data),
on: (channel: string, cb: Function) => ipcRenderer.on(channel, (_e, ...args) => cb(...args)),
})
渲染侧一旦有 XSS,攻击者可以调用任何 IPC 通道(包括内部调试通道、未来新增的危险通道)。正解是通道白名单 + 参数签名收敛:
ts
// ✅ 每个对外能力是一个具名、窄参数、只读必要数据的函数
const ALLOWED_INVOKE = new Set([
'project.open', 'project.save',
'media.selectLocalMediaFiles',
'renderExport.start', 'renderExport.cancel',
// ......显式枚举,新通道默认不暴露,安全评审后才加入
])
contextBridge.exposeInMainWorld('magicut', {
openProject: (projectPath: string) => {
if (typeof projectPath !== 'string') throw new TypeError('invalid path')
return ipcRenderer.invoke('project.open', { projectPath })
},
startRender: (params: RenderParams) =>
ipcRenderer.invoke('renderExport.start', params),
})
四条规则:
- 白名单枚举通道,禁止透传 channel 字符串。
- 参数在桥上过一道类型检查,不信任渲染侧任何输入(主进程仍要 Zod 复核,纵深防御)。
- 不暴露
on这种万能订阅:事件回传同样按具名事件 + 单一注册入口暴露,返回取消订阅函数。 - 不把主进程对象/函数直接塞进 bridge:contextBridge 对原型链有复制语义,泄漏的对象能力不可预测;只传可结构化克隆的纯数据与薄函数。
四、第三层:导航与新窗口控制
XSS 的另一条路径是把页面导航到攻击者站点(钓鱼)或借 window.open 创建一个带 Node 权限的新窗口:
ts
app.on('web-contents-created', (_event, contents) => {
// 禁止任意导航:只允许应用内页面
contents.on('will-navigate', (e, url) => {
if (!isInternalUrl(url)) e.preventDefault()
})
// 所有 window.open / target=_blank 交系统浏览器,绝不在应用内开新 Electron 窗
contents.setWindowOpenHandler(({ url }) => {
if (isExternalHttpUrl(url)) {
shell.openExternal(url)
}
return { action: 'deny' }
})
// 阻止 webview 标签被注入(不使用 webview 时直接禁用)
contents.on('will-attach-webview', (e, webPreferences, params) => {
e.preventDefault()
})
})
shell.openExternal 本身也要校验协议(只允许 http/https/mailto)------file://、自定义协议甚至操作系统伪协议(smb://、Windows 的 ms- 系列)都可能被利用来执行或钓鱼。
五、第四层:CSP 与 XSS 防御
CSP:即便注入了脚本也不让它跑
http
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' app-media: data: blob:;
media-src 'self' app-media: blob:;
connect-src 'self' https://api.your-ai-domain.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
要点:
- 不用
unsafe-eval/远程 script:Vue 3 生产构建不需要 eval(开发模式的 HMR 才需要,通过开发环境单独 CSP 放开,生产构建不带)。 - 自定义媒体协议显式列入 :
app-media:是视频/图片来源(见自定义协议篇),不加会被 default-src 拦掉。 - AI API 域名收敛 :
connect-src只放真实后端,注入脚本想把数据外传(exfiltration)到攻击者服务器会被浏览器直接阻断------这是 XSS 之后的止血带。 - CSP 通过
session.defaultSession.webRequest.onHeadersReceived注入或写在 HTML meta;生产强制响应头,防 meta 被绕过。
XSS 本体:数据渲染的默认安全
CSP 是兜底,根因还要堵:
- 默认文本渲染 :Vue 的
{{ }}自动转义,永远不要为了渲染 SRT/AI 文本用v-html。 - 富文本必须消毒:如果产品要支持富文本(带样式的字幕模板),用 DOMPurify 类库白名单消毒后再渲染,白名单只留展示标签(b/i/font/span),禁事件属性(on*)与 javascript: 协议。
- 文件内容按文本处理 :SRT/ASS 解析不要拼 DOM;字幕渲染优先走 canvas 或严格转义后的 DOM。AI 返回内容过 Zod schema(
z.string()天然剥离对象注入)后再展示。 - 下载文件名同样危险 :
download属性、a.title里注入换行/控制字符可做 HTTP 响应拆分类攻击,统一消毒。
六、第五层:IPC 与文件系统边界
桥再窄,主进程的 IPC handler 也是最后一道门:
- 入参 Zod 校验(见 IPC 契约篇)------渲染进程被攻陷后,畸形/恶意参数在主进程被挡。
- 路径能力白名单 :文件读写只接受应用管理的目录(项目目录、缓存目录),自定义协议做路径穿越防护(
..normalize + 根目录前缀校验)。 - 对话框代表用户授权 :读任意文件先走系统
dialog.showOpenDialog(用户显式选择),而不是给渲染进程一个"读任意路径"的 IPC。安全模型是能力授予(capability-based):用户每选一次文件,授予该路径的访问权。 - 禁用危险 webPreferences 组合 :
webviewTag:false、enableRemoteModule不存在于现代版本(remote 模块在新版已移除,不要装回 @electron/remote)。 - 权限 API 收敛 :
session.setPermissionRequestHandler对摄像头/麦克风/通知按功能白名单裁决,默认拒绝。
七、供应链与分发安全
- 依赖审计 :Electron 应用更新频繁,CI 跑
npm audit+ 针对 Electron 生态的安全公告订阅;electron本身升级优先级最高(Chromium 漏洞跟随)。 - asar 完整性与代码签名:Windows Authenticode、macOS 公证(notarization)。签名防止安装包被篡改后植入------用户侧 SmartScreen/Gatekeeper 也依赖它。
- 自定义协议特权最小化 :注册特权协议时
bypassCSP默认 false、corsEnabled按需要,不要一把全开。 - 更新通道走 HTTPS 且验签(见自动更新篇):更新机制本身是最高价值目标,被劫持即全员 RCE。
八、安全自检清单(发版门禁)
| 项 | 期望 |
|---|---|
| contextIsolation / nodeIntegration / sandbox | true / false / true |
| preload 暴露面 | 具名函数白名单,无透传 send/invoke/on |
| will-navigate / setWindowOpenHandler | 仅内部导航,外链交系统浏览器 |
| CSP | 无 unsafe-eval,connect-src 收敛,object-src none |
| v-html | 全局 grep 为零或全部经消毒 |
| IPC | 全部 channel 在白名单 + Zod 入参 |
| 文件访问 | 白名单根目录 + 用户对话框授权 |
| 权限请求 | 默认拒绝 |
| 签名/公证/更新 | 全平台签名,更新包验签 |
九、小结
Electron 安全没有银弹,是五层纵深:加固渲染器(让 XSS 拿不到 Node)→ 收窄桥接(拿到了也调不了什么)→ 锁死导航(跳不到钓鱼页)→ CSP 与转义(脚本跑不起来、数据偷不出去)→ 主进程能力边界(进来的参数和路径都不可信)。任何一层被绕过,后面的层继续生效。
最该记住的心智模型是零信任渲染器:假设 UI 进程已经被攻陷,整个架构的其余部分是否仍然安全?用这个问题审视每一个新 IPC、每一行 v-html、每一个新增的 preload 方法,安全就从"发版前检查表"变成了"设计时的默认姿势"。