Electron 安全纵深:contextIsolation、最小暴露面与 CSP 防线

Electron 安全纵深:contextIsolation、最小暴露面与 CSP 防线

Electron 应用同时拥有 Chromium 的完整 Web 能力和 Node.js 的完整系统权限------这意味着一个 XSS 就等于任意代码执行:读取用户全部文件、调用子进程、持久化驻留。安全不是发版前的一次检查,而是贯穿架构的纵深防线。本文拆解 Electron 安全模型的五层防御:渲染器加固、桥接最小化、导航控制、内容安全策略与文件系统边界。

一、威胁模型:先认清攻击者从哪进来

视频编辑器看似纯本地工具,攻击面却不少:

  1. 导入即触发 :用户导入第三方字幕(SRT/ASS)、项目文件、素材元数据------这些都是攻击者可控文本,会被渲染到 UI。
  2. AI 内容回流:LLM 转写、TTS、分析结果含攻击者控制的文本(恶意视频里的 ASR 结果就是攻击载体)。
  3. 远程内容:更新公告、模板市场、帮助页如果加载远程网页,供应链风险进入。
  4. 供应链: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),
})

四条规则:

  1. 白名单枚举通道,禁止透传 channel 字符串。
  2. 参数在桥上过一道类型检查,不信任渲染侧任何输入(主进程仍要 Zod 复核,纵深防御)。
  3. 不暴露 on 这种万能订阅:事件回传同样按具名事件 + 单一注册入口暴露,返回取消订阅函数。
  4. 不把主进程对象/函数直接塞进 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 也是最后一道门:

  1. 入参 Zod 校验(见 IPC 契约篇)------渲染进程被攻陷后,畸形/恶意参数在主进程被挡。
  2. 路径能力白名单 :文件读写只接受应用管理的目录(项目目录、缓存目录),自定义协议做路径穿越防护(.. normalize + 根目录前缀校验)。
  3. 对话框代表用户授权 :读任意文件先走系统 dialog.showOpenDialog(用户显式选择),而不是给渲染进程一个"读任意路径"的 IPC。安全模型是能力授予(capability-based):用户每选一次文件,授予该路径的访问权。
  4. 禁用危险 webPreferences 组合webviewTag:falseenableRemoteModule 不存在于现代版本(remote 模块在新版已移除,不要装回 @electron/remote)。
  5. 权限 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 方法,安全就从"发版前检查表"变成了"设计时的默认姿势"。

相关推荐
凤城老人1 天前
从零手写开源私有云盘|Flask+Vue3+Electron全栈网盘,单容器一键部署
python·docker·electron·vue
flash俊杰2 天前
Electron 自定义协议与视频流式加载:从 moov atom 到 Range 请求的工程化实践
electron·前端框架
愛芳芳2 天前
基于 Electron + Vue3 的仿 PC 微信客户端项目实战
前端·javascript·css·elementui·typescript·electron·vue
Patrick_Wilson4 天前
桌面端发布的真实成本:签名、公证和那支不能共享的 USB Key
ci/cd·electron·客户端
爱丶不疚4 天前
Electron: 你是否需要对 Preload 开启 nodeIntegration?
安全·性能优化·electron
爱丶不疚4 天前
Electron:Module 与 Service 的职责边界与加载时序编排
前端·electron·nestjs
爱丶不疚4 天前
Electron: 缓存机制有哪些?能控制的又有哪些
前端·electron·v8
忆凡_6 天前
Dyad 鸿蒙 PC 适配全记录:让本地 AI 应用构建器进入 HarmonyOS PC
electron·鸿蒙
心态还需努力呀7 天前
把 Zettlr 搬上鸿蒙 PC:一次 Electron 应用移植实战
华为·electron·harmonyos