Electron 安全模型:nodeIntegration、contextIsolation 与 preload 脚本

Electron 应用的危险之处在于:一旦把浏览器页面和本地 Node 能力直接绑在一起,只要出现一处 XSS,就等于给任意脚本开了本地后门。
这一篇聚焦 Electron 的安全模型,围绕三件事展开:nodeIntegration 到底控制了什么、contextIsolation 为什么重要,以及应该如何用 preload 脚本和 contextBridge 暴露受控的本地 API。

在浏览器里,即便存在 XSS 漏洞,攻击者的能力通常被限制在「同源策略」范围内:

  • 能读写当前站点的 Cookie / LocalStorage;
  • 能发起网络请求(受 CORS/CSRF 等约束);
  • 不能直接访问本地文件系统,更不能随意启动本地程序。

而在 Electron 中,如果不做限制:

  • 渲染进程里的 JS 既能访问 DOM,又能直接用 Node API 读写本地文件、执行命令、访问内网;
  • 一旦页面存在 XSS,相当于给攻击者一个带本地权限的「浏览器壳」。

安全设计的核心目标可以概括为:

  • 让渲染进程尽量像普通浏览器页面,只通过一个受控的接口访问本地能力。

nodeIntegration 决定了渲染进程中是否可以直接使用 Node:

  • nodeIntegration: true
    • 可以在页面脚本里直接 require('fs')require('child_process')
    • 也就意味着任何注入到页面里的脚本都可以这样做。
  • nodeIntegration: false
    • 页面中没有直接的 require / process / __dirname 等 Node 全局对象;
    • 想使用本地能力,必须通过预先暴露的接口。

出于安全考虑,推荐的默认配置是:

  • 所有渲染进程一律 nodeIntegration: false

如果确实有极特殊场景需要开启,也应该:

  • 只针对特定内部页面;
  • 严格控制这些页面的加载来源(本地打包资源),禁止远程 URL 和任意 HTML 注入。

如果同时存在:

  • nodeIntegration: true
  • 任意一种 XSS(用户输入未转义、第三方脚本不可信等),

那么攻击者可以直接在渲染进程中执行任意 Node 代码,例如:

1require("child_process").exec("rm -rf /some/important/path");

这就是把「前端安全问题」升级成了「本地系统安全问题」。

contextIsolation 控制的是:

  • 页面脚本(运行在网页上下文里)和
  • 预加载脚本(preload,运行在一个独立的 JS 上下文里)

之间是否共享同一个全局对象(window)。

  • contextIsolation: false
    • 页面脚本和 preload 脚本共享同一个 window
    • preload 往 window 上挂的对象可以被页面脚本直接修改或覆盖。
  • contextIsolation: true
    • 页面脚本运行在一个隔离的上下文;
    • preload 可以通过 contextBridge 把只读接口「桥接」到页面上下文中;
    • 页面脚本无法随意篡改这些接口的底层实现。

推荐默认是:

  • contextIsolation: true,配合 nodeIntegration: false 一起使用。

在这种安全模式下:

  • 渲染进程的页面本身拿不到 Node API;
  • 也无法直接访问主进程;
  • 只能通过 preload 暴露的有限 API,间接访问本地能力。

这样可以强制你为每一类本地访问设计一个清晰的「门面」:

  • 在这一层做参数验证、权限控制、日志审计;
  • 再决定是否把请求转发给主进程或后端服务。

preload 脚本运行在渲染进程加载页面之前,用来:

  • 执行一些初始化逻辑;
  • 通过 ipcRenderer 与主进程建立通信通道;
  • contextBridge.exposeInMainWorld 暴露有限的全局 API 给页面使用。

常见结构可以抽象成:

1// preload.js
2const { contextBridge, ipcRenderer } = require("electron");
3
4contextBridge.exposeInMainWorld("appApi", {
5  readConfig: () => ipcRenderer.invoke("config:read"),
6  writeConfig: (data) => ipcRenderer.invoke("config:write", data),
7});

页面脚本里则使用:

1// 渲染进程(前端应用)
2window.appApi.readConfig().then((cfg) => {
3  // ...
4});

页面本身看不到 ipcRenderer 和 Node,只能调用 appApi 暴露出来的这几个方法。

在 preload 和主进程之间,可以做很多工程上的安全控制,例如:

  • 对传入参数做类型和范围检查;
  • 对敏感操作(删除文件、执行命令)增加额外的确认或权限判断;
  • 为每个 API 接口打日志,记录调用方、参数和结果;
  • 对 IPC 通道做命名约束,防止随意约定字符串导致混乱。

总体思路是:

  • 把 preload + 主进程这一层,当成一个「本地后端」,渲染进程只是一个客户端。

除了 nodeIntegration / contextIsolation / preload 之外,Electron 安全还包括很多维度,这里做一个简要 checklist:

  • 不要加载不受信任的远程 URL 到有本地能力的窗口中
  • 对所有用户输入进行严格转义,防止 XSS
  • 使用 Content-Security-Policy(CSP)限制脚本来源;
  • 尽量避免 evalnew FunctioninnerHTML 直接拼接未信任内容;
  • 对自动更新、插件/脚本加载等功能做完整性校验(签名/哈希)。

这些原则和 Web 安全有大量重叠,只是因为 Electron 有更高权限,所有问题的后果被放大了一个量级。

可以用几条简单的底线来概括这一篇内容:

  • 渲染进程默认关闭 Node 能力(nodeIntegration: false),不要把 Node API 暴露给任意页面脚本
  • 打开上下文隔离(contextIsolation: true),通过 preload + contextBridge 暴露有限、本地 API 化的接口;
  • 在本地 API 层做参数验证、权限控制与审计,把真正的危险操作收紧到最小范围。

在这个安全基线之上,再去考虑性能优化、原生集成与复杂业务逻辑,Electron 应用的整体风险会可控得多。