Electron 安全模型:nodeIntegration、contextIsolation 与 preload 脚本
Electron 应用的危险之处在于:一旦把浏览器页面和本地 Node 能力直接绑在一起,只要出现一处 XSS,就等于给任意脚本开了本地后门。
这一篇聚焦 Electron 的安全模型,围绕三件事展开:nodeIntegration到底控制了什么、contextIsolation为什么重要,以及应该如何用 preload 脚本和contextBridge暴露受控的本地 API。
1. 为什么 Electron 的安全风险和普通 Web 不一样?
在浏览器里,即便存在 XSS 漏洞,攻击者的能力通常被限制在「同源策略」范围内:
- 能读写当前站点的 Cookie / LocalStorage;
- 能发起网络请求(受 CORS/CSRF 等约束);
- 不能直接访问本地文件系统,更不能随意启动本地程序。
而在 Electron 中,如果不做限制:
- 渲染进程里的 JS 既能访问 DOM,又能直接用 Node API 读写本地文件、执行命令、访问内网;
- 一旦页面存在 XSS,相当于给攻击者一个带本地权限的「浏览器壳」。
安全设计的核心目标可以概括为:
- 让渲染进程尽量像普通浏览器页面,只通过一个受控的接口访问本地能力。
2. nodeIntegration:不要让渲染进程直接拿到 Node
2.1 nodeIntegration 做了什么?
nodeIntegration 决定了渲染进程中是否可以直接使用 Node:
nodeIntegration: true:- 可以在页面脚本里直接
require('fs')、require('child_process'); - 也就意味着任何注入到页面里的脚本都可以这样做。
- 可以在页面脚本里直接
nodeIntegration: false:- 页面中没有直接的
require/process/__dirname等 Node 全局对象; - 想使用本地能力,必须通过预先暴露的接口。
- 页面中没有直接的
出于安全考虑,推荐的默认配置是:
- 所有渲染进程一律
nodeIntegration: false。
如果确实有极特殊场景需要开启,也应该:
- 只针对特定内部页面;
- 严格控制这些页面的加载来源(本地打包资源),禁止远程 URL 和任意 HTML 注入。
2.2 和 XSS 结合时的风险
如果同时存在:
nodeIntegration: true;- 任意一种 XSS(用户输入未转义、第三方脚本不可信等),
那么攻击者可以直接在渲染进程中执行任意 Node 代码,例如:
1require("child_process").exec("rm -rf /some/important/path");
这就是把「前端安全问题」升级成了「本地系统安全问题」。
3. contextIsolation:隔离页面脚本和 preload 脚本
3.1 contextIsolation 的作用
contextIsolation 控制的是:
- 页面脚本(运行在网页上下文里)和
- 预加载脚本(preload,运行在一个独立的 JS 上下文里)
之间是否共享同一个全局对象(window)。
contextIsolation: false:- 页面脚本和 preload 脚本共享同一个
window; - preload 往
window上挂的对象可以被页面脚本直接修改或覆盖。
- 页面脚本和 preload 脚本共享同一个
contextIsolation: true:- 页面脚本运行在一个隔离的上下文;
- preload 可以通过
contextBridge把只读接口「桥接」到页面上下文中; - 页面脚本无法随意篡改这些接口的底层实现。
推荐默认是:
contextIsolation: true,配合nodeIntegration: false一起使用。
3.2 为什么要配合 preload 脚本使用?
在这种安全模式下:
- 渲染进程的页面本身拿不到 Node API;
- 也无法直接访问主进程;
- 只能通过 preload 暴露的有限 API,间接访问本地能力。
这样可以强制你为每一类本地访问设计一个清晰的「门面」:
- 在这一层做参数验证、权限控制、日志审计;
- 再决定是否把请求转发给主进程或后端服务。
4. preload 脚本与 contextBridge:构建受控的本地 API
4.1 preload 脚本的角色
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 暴露出来的这几个方法。
4.2 在这一层做哪些安全设计?
在 preload 和主进程之间,可以做很多工程上的安全控制,例如:
- 对传入参数做类型和范围检查;
- 对敏感操作(删除文件、执行命令)增加额外的确认或权限判断;
- 为每个 API 接口打日志,记录调用方、参数和结果;
- 对 IPC 通道做命名约束,防止随意约定字符串导致混乱。
总体思路是:
- 把 preload + 主进程这一层,当成一个「本地后端」,渲染进程只是一个客户端。
5. 其他关键安全实践(简要罗列)
除了 nodeIntegration / contextIsolation / preload 之外,Electron 安全还包括很多维度,这里做一个简要 checklist:
- 不要加载不受信任的远程 URL 到有本地能力的窗口中;
- 对所有用户输入进行严格转义,防止 XSS;
- 使用
Content-Security-Policy(CSP)限制脚本来源; - 尽量避免
eval、new Function、innerHTML直接拼接未信任内容; - 对自动更新、插件/脚本加载等功能做完整性校验(签名/哈希)。
这些原则和 Web 安全有大量重叠,只是因为 Electron 有更高权限,所有问题的后果被放大了一个量级。
6. 小结:Electron 应用安全的几条底线
可以用几条简单的底线来概括这一篇内容:
- 渲染进程默认关闭 Node 能力(
nodeIntegration: false),不要把 Node API 暴露给任意页面脚本; - 打开上下文隔离(
contextIsolation: true),通过 preload +contextBridge暴露有限、本地 API 化的接口; - 在本地 API 层做参数验证、权限控制与审计,把真正的危险操作收紧到最小范围。
在这个安全基线之上,再去考虑性能优化、原生集成与复杂业务逻辑,Electron 应用的整体风险会可控得多。