Electron 安全最佳实践
本教程共 45 篇 · 第 39 篇 · 更新于 2026-08-03
39. Electron 安全最佳实践
本节目标
- 理解 Electron 比浏览器更危险的根本原因。
- 掌握官方 20 条安全清单的核心条目。
- 弄清
contextIsolation、nodeIntegration、sandbox三者的关系。 - 学会谨慎加载远程内容并最小化权限。
- 知道如何在校验脚本里落实这些默认项。
1-1 为什么 Electron 的安全格外重要
浏览器为网页开发者提供了一张牢固的安全网。我们写的网页被关在沙箱里,权限很小,而且背后有一支庞大的工程师团队在快速修复新出现的漏洞。换句话说,哪怕代码出了错,破坏范围也有限。
Electron 不一样。它让你用熟悉的 Web 技术构建功能丰富的桌面应用,但你的代码拥有大得多的权力。JavaScript 可以直接读写文件系统、调用系统 shell、操作剪贴板。这种能力是把双刃剑:它让高质量的桌面应用成为可能,也让安全风险随权力一起放大。
一个关键点必须记牢:Electron 不是浏览器。当你把来自不可信来源的内容(例如远程服务器)加载进应用并执行时,就埋下了严重隐患。事实上,最流行的那些 Electron 应用,比如 VS Code、Slack、Atom,主要展示的都是本地内容,或者经过严格审查、且关闭了 Node 集成的远程内容。
提示:如果你要展示的是完全不可信的网页,浏览器本身比你的 Electron 应用更安全。Electron 适合展示可信内容,或经过隔离的远程内容。
1-2 安全清单:20 条基线
官方文档给出了一份安全清单,至少要做到下面这些条目。这里挑出最常踩坑的,逐条说明为什么以及怎么做。完整 20 条可到 Electron 官方安全文档查看。
- 只加载安全内容(HTTPS)。
- 不要为远程内容启用 Node.js 集成。
- 在所有渲染进程启用上下文隔离。
- 启用进程沙箱。
- 为加载远程内容的会话设置
setPermissionRequestHandler。 - 不要禁用
webSecurity。 - 定义严格的
Content-Security-Policy。 - 不要启用
allowRunningInsecureContent。 - 不要启用实验性特性。
- 不要使用
enableBlinkFeatures。 <webview>不要使用allowpopups。- 校验
<webview>的选项与参数。 - 禁用或限制导航。
- 禁用或限制新建窗口。
- 不要用
shell.openExternal打开不可信内容。 - 使用较新版本的 Electron。
- 校验所有 IPC 消息的发送方。
- 避免直接使用
file://协议,优先自定义协议。 - 检查可关闭的 Fuses(熔断开关)。
- 不要把 Electron API 暴露给不可信的 Web 内容。
下面用一段主进程代码,把最常见的几个默认项一次性设好。注意 contextIsolation、nodeIntegration、sandbox 都是显式写明,而不是依赖记忆。
// 主进程 main.js
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
width: 1000,
height: 700,
webPreferences: {
// 沙箱:进一步收紧渲染进程的操作系统权限
sandbox: true,
// 上下文隔离:默认即 true,这里显式写出以强调
contextIsolation: true,
// 不把 Node 能力暴露给渲染进程
nodeIntegration: false,
// 用 preload 做受控桥接,而不是开 nodeIntegration
preload: require('node:path').join(app.getAppPath(), 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
1-3 contextIsolation:上下文隔离
上下文隔离是 Electron 的一项特性,它让 preload 脚本和 Electron 内部 API 运行在一个独立的 JavaScript 上下文里。实践上这意味着:渲染进程里的脚本无法篡改 Array.prototype.push、JSON.parse 这类全局对象。Electron 用的是和 Chromium 内容脚本相同的技术来实现这一点。
即便你设了 nodeIntegration: false,要真正隔离、杜绝 Node 原语的泄漏,仍然必须启用 contextIsolation。它的默认值自 Electron 12 起就是 true,所以在 v43.2.0 里你几乎不必写它——但显式写出能提醒自己和协作者:这是底线。
预加载脚本不能像下面这样直接往 window 上挂变量,因为隔离默认开启:
// ❌ preload.js(错误写法:变量会被隔离,渲染进程读不到)
window.myAPI = { desktop: true }
// ✅ preload.js(正确写法:用 contextBridge 暴露)
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
提示:关闭上下文隔离不仅危险,还会连带关闭进程沙箱,无论你是否显式设置了
sandbox: false或全局开启沙箱。
1-4 nodeIntegration 与远程内容
绝不可以在加载远程内容的渲染进程里启用 Node.js 集成。这里的远程内容指 BrowserWindow、WebContentsView、<webview> 加载的不受你完全控制的网址。关闭 Node 集成后,攻击者即便通过 XSS 注入脚本,也无法直接调用 require、访问文件系统,从而把攻击挡在渲染进程内。
下面用一个对照说明「错误配置」与「正确做法」的本质差别。注意左侧的危险写法刻意不写出具体值:它指的是那种为远程内容「关闭上下文隔离、打开 Node 集成、连 Worker 也打开 Node」的组合,这类组合会把渲染进程变成任人操控的远端执行环境。
// ❌ 主进程:不要这样做(危险示例,请勿照抄)
// 所谓“危险配置”,指为远程内容关闭隔离、开启 Node 集成(含 Worker)
// 这类写法会让远程网页拿到 require、fs 等 Node 能力,绝对不能写
const bad = new BrowserWindow({
webPreferences: {
// 这里不应出现“开启 Node 集成 / 关闭隔离”的任何配置项
}
})
bad.loadURL('https://example.com')
// ✅ 主进程(安全:用 preload 受控桥接)
const good = new BrowserWindow({
webPreferences: {
preload: require('node:path').join(app.getAppPath(), 'preload.js')
// contextIsolation / nodeIntegration 使用 v43 默认即可
}
})
good.loadURL('https://example.com')
关闭 Node 集成后,你依然可以通过 preload 暴露受控 API。preload 仍保有 require 与 Node 能力,再用 contextBridge 把需要的少数函数交给页面,这是官方推荐的做法。
1-5 sandbox:进程沙箱
沙箱是 Chromium 的功能,借助操作系统把渲染进程能访问的东西大幅收窄。它自 Electron 20 起默认开启,但建议你对每个渲染进程显式启用,并且可以全局强制开启。所有加载、读取或处理不可信内容的工作,都应当在已沙箱的进程里完成;主进程本身并不享受沙箱保护,所以主进程绝不能处理不可信内容。
在 sandbox: true 下,preload 脚本能用的 Node API 会变少(比如无法使用 require 加载部分模块),因此把重逻辑放在主进程,preload 只做最小桥接,是更稳妥的分工。
1-6 谨慎加载远程内容
远程内容必须走 HTTPS,不要使用 HTTP。HTTPS 既能保证数据在传输中不被篡改,也能加密流量,让窃听更困难。同理,优先 WSS 而非 WS、FTPS 而非 FTP。
如果要在应用里嵌入远程网页,优先用 <webview> 或 WebContentsView,并且务必禁用 nodeIntegration、启用 contextIsolation。同时,建议用 will-attach-webview 事件在创建前拦截并校验选项,防止页面脚本擅自用危险配置新建 webview。
// 主进程 main.js
const { app } = require('electron')
app.on('web-contents-created', (event, contents) => {
contents.on('will-attach-webview', (e, webPreferences, params) => {
// 移除不必要的 preload,或校验其来源合法
delete webPreferences.preload
// 关闭 Node 集成
webPreferences.nodeIntegration = false
// 只允许可信来源
if (!params.src.startsWith('https://example.com/')) {
e.preventDefault()
}
})
})
此外,建议限制导航与新建窗口。导航是常见攻击入口:一旦攻击者诱导你的应用跳到外部站点,隔离就容易被绕过。下面的示例用 will-navigate 把导航限制到可信源。
// 主进程 main.js
const { app } = require('electron')
const { URL: NodeURL } = require('node:url')
app.on('web-contents-created', (event, contents) => {
contents.on('will-navigate', (e, navigationUrl) => {
const parsed = new NodeURL(navigationUrl)
if (parsed.origin !== 'https://example.com') {
e.preventDefault()
}
})
})
1-7 校验 IPC 发送方与最小权限
所有 Web 框架理论上都能向主进程发 IPC 消息,包括 iframe 和子窗口。如果你有返回用户数据或执行特权操作的 IPC 处理器,就必须校验发送方,避免把信息泄露给第三方框架。
// ❌ 主进程:不要这样做(危险:任何渲染框都能拿到密钥)
ipcMain.handle('get-secrets', () => {
return getSecrets()
})
// ✅ 主进程(安全:先校验 senderFrame 的来源)
ipcMain.handle('get-secrets', (e) => {
if (!validateSender(e.senderFrame)) return null
return getSecrets()
})
function validateSender (frame) {
if (!frame) return false
// 用真正的 URL 解析 + 白名单判断 host
try {
return new (require('node:url').URL)(frame.url).host === 'example.com'
} catch {
return false
}
}
还有一个容易忽略的点:不要把原生的 ipcRenderer.on 直接通过 contextBridge 暴露给不可信内容。那样渲染进程就能监听任意 IPC 事件。应当只暴露你真正需要的包装函数,并且在回调里把事件对象过滤掉,只传业务数据。
小结
Electron 的安全是框架、Chromium、Node.js、依赖库和你自己的代码共同决定的结果。最容易做到也最该坚持的,是把 contextIsolation: true、nodeIntegration: false、sandbox: true 作为默认,再叠加 HTTPS、CSP、权限最小化与 IPC 发送方校验。下一章我们会看几种具体的攻击手法,以及它们如何被这些默认项化解。