很多 Electron 应用并不是完全离线的,它们会加载远程页面——比如嵌入 Web 版的后台管理界面、显示第三方网页、或者直接把自己的渲染进程指向一个线上的前端资源。远程内容意味着你失去了对执行环境的完全控制,任何来自网络的内容都有可能引入 XSS、CSRF、甚至远程代码执行的攻击。本章不会复述通用 Web 安全知识,而是聚焦在 Electron 特有的、因远程页面加载而引入的风险以及对应的防护手段。
23.4.1 风险全景:当远程内容进入 Electron
先直观感受一下危险有多大。一个典型的错误做法是:
// ❌ 危险写法:直接加载不可控的远程页面
mainWindow.loadURL('https://example.com/app');
如果 example.com 被劫持,或者返回的 HTML 中包含了恶意脚本,那这个脚本就运行在了你的渲染进程中。此时取决于你的配置,攻击者可能能够:
- 直接使用 Node.js 全部 API(如果
nodeIntegration开启),读写本地文件、执行系统命令; - 通过
shell.openExternal打开恶意链接; - 发送 IPC 消息到主进程,触发高危操作;
- 访问
remote模块(如果开启),直接操作主进程对象。
即使你的应用本身业务逻辑是安全的,一旦加载了不可信的远程资源,安全边界就会瞬间瓦解。所以,远程页面安全的核心原则是:永远假设远程页面已经被攻破,用最小权限原则来限制它的能力,降低它能够造成的破坏。
23.4.2 安全校验:只加载你信任的内容
1. 严格限制加载来源
最基础的防范就是只加载你完全控制的域名,并且使用 HTTPS。不要在应用中允许用户输入任意 URL 然后直接渲染。如果必须加载第三方页面(比如帮助文档、博客),建议通过白名单机制:
const ALLOWED_ORIGINS = [
'https://yourapp.com',
'https://docs.yourapp.com',
];
function isAllowed(url) {
try {
const parsed = new URL(url);
return ALLOWED_ORIGINS.includes(parsed.origin);
} catch {
return false;
}
}
// 在加载或导航事件中校验
win.webContents.on('will-navigate', (event, url) => {
if (!isAllowed(url)) {
event.preventDefault();
// 可以跳转到安全提示页面
}
});
win.webContents.on('will-redirect', (event, url) => {
if (!isAllowed(url)) {
event.preventDefault();
}
});
同时也要拦截新窗口的打开,防止绕过当前窗口的限制:
win.webContents.setWindowOpenHandler(({ url }) => {
if (isAllowed(url)) {
return { action: 'allow' };
}
return { action: 'deny' };
});
2. 内容安全策略(CSP)
CSP 是你的最后一道防线。在渲染进程中通过 <meta> 标签或 HTTP 响应头设置严格的 CSP,可以极大限制能够执行的脚本来源。即使远程页面被注入了恶意脚本,只要 CSP 不包含该来源,攻击代码就无法运行。
一个适用于远程内容的 CSP 示例:
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://trusted.cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.yourapp.com;
frame-src 'none';
">
要点:
script-src绝对不要包含'unsafe-inline'或'unsafe-eval',除非你清楚风险并必须使用(如果必须用,考虑使用 nonce 或 hash 方案)。- 禁止
frame-src或者限制为可信来源,防止点击劫持。 - 通过
connect-src限制网络请求目标,防止数据外泄。
也可以在 Electron 的主进程中通过注册 'html' 拦截器来注入 CSP 头:
// 主进程,为每个响应注入 CSP 头
const { session } = require('electron');
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'"
]
}
});
});
但注意:如果远程页面本身就通过 HTTP 头返回了 CSP,你需要合并策略,通常采用追加限制的方式。
3. 导航与跳转控制
除了 will-navigate,还需要处理以下场景:
will-redirect:处理服务器重定向new-window事件:通过setWindowOpenHandler统一拦截will-attach-webview:如果你使用了<webview>标签加载远程页面,必须严格控制其src,并同样设置内部 CSP 和权限。
23.4.3 权限限制:把能力关进笼子
远程页面加载时,必须将 Electron 的“超级权限”严格收缩。现在官方的最佳实践是:
// ✅ 安全配置:为所有窗口设置安全基线
const mainWindow = new BrowserWindow({
webPreferences: {
// 彻底关闭 Node.js 集成
nodeIntegration: false,
// 禁止使用 remote 模块(Electron 14+ 默认禁用)
enableRemoteModule: false,
// 开启上下文隔离
contextIsolation: true,
// 开启沙盒(见下一节)
sandbox: true,
// 使用预加载脚本暴露受控的 API
preload: path.join(__dirname, 'preload.js'),
// 禁止打开 devTools(生产环境)
devTools: false,
// 禁用 webview 标签(如果不需要)
webviewTag: false,
}
});
这些配置项各自的作用:
nodeIntegration: false:渲染进程不再有require、process、__dirname等 Node 全局变量,恶意脚本无法直接调用系统模块。contextIsolation: true:预加载脚本和页面脚本运行在独立的世界中,页面无法直接访问预加载中暴露的 Electron API 内部变量,只能通过contextBridge获取你明确暴露的方法。enableRemoteModule: false:避免远程内容通过remote模块直接操作主进程对象。sandbox: true(详见 23.4.4 节):在操作系统层面限制进程能力,即使脚本突破了 Node 限制,也无法执行高危系统调用。devTools: false:防止用户或攻击者打开 DevTools 调试和修改代码。webviewTag: false:<webview>标签本身是一个独立的 renderer,管理不当可能成为安全漏洞,禁用后可避免相关风险。
预加载脚本是权限接口的守门人。 你应该只暴露应用业务需要的、最小化的方法:
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('appAPI', {
// 只暴露一个打开文件对话框的功能
openFileDialog: () => ipcRenderer.invoke('dialog:openFile'),
// 暴露一个只读的配置获取方法
getConfig: (key) => ipcRenderer.invoke('config:get', key),
// 绝不要暴露类似下面的危险方法
// runCommand: (cmd) => ipcRenderer.invoke('runCommand', cmd) // 危险!
});
然后在主进程中对 IPC 调用做严格的参数校验和权限判断,绝不能盲目信任渲染进程发来的数据。
23.4.4 沙箱隔离:最后的物理屏障
Electron 的 sandbox 选项基于 Chromium 的沙箱技术,它在操作系统层面将渲染进程隔离开来。开启沙箱后,渲染进程的能力受到极大限制:
- 无法直接访问文件系统(即使有 Node 的模块,底层系统调用也会被拦截,除非通过主进程的 IPC 代理);
- 无法创建子进程;
- 网络访问也会受限于沙箱策略(但通常允许通过主进程的代理)。
开启沙箱是当前官方强烈推荐的做法,但它会带来一些兼容性限制:
- 预加载脚本本身也在沙箱中运行,因此它的
require能力会受到限制。你可能需要使用esModule或在打包时处理某些原生模块。 - 某些需要直接调用系统库的 Node 模块可能无法工作,必须通过主进程代理。
- 如果你的应用使用了
<webview>,它也应该是沙箱化的,但配置会复杂一些,多数现代应用已弃用<webview>。
开启方式非常简单:
webPreferences: {
sandbox: true,
// 注意:sandbox: true 时,contextIsolation 也必须为 true
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
}
在实际项目中,你可以逐步推进。如果由于历史原因不能立即对主窗口开启沙箱,至少应该为加载远程内容的窗口开启沙箱,因为它的风险面最大。
一个实用技巧:如果必须加载不可控的第三方页面,最安全的方式是将它们放入一个独立的、完全沙箱化的 BrowserView 或 iframe(配合 sandbox 属性,但要注意 iframe 的 Electron 能力限制)。例如,可以创建一个不附加 preload、sandbox 开启的窗口,仅用来展示第三方内容,并通过 IPC 与主应用做有限的数据交换。
23.4.5 实践清单
最后,用一份可操作的检查单确保你的远程页面加载足够安全:
- [ ] 所有远程页面均通过 HTTPS 加载。
- [ ] 实现了严格的 URL 白名单校验,覆盖导航、重定向和新窗口。
- [ ] 设置了严格的 CSP,禁止不安全的脚本来源和内联执行。
- [ ] 渲染进程配置满足:
nodeIntegration: false、contextIsolation: true、sandbox: true。 - [ ] 预加载脚本仅暴露最小且安全的 API,所有 IPC 消息在主进程侧做参数校验。
- [ ] 禁用了
remote模块、devTools(生产环境)和不需要的webviewTag。 - [ ] 对用户输入或传递到主进程的数据始终做转义和类型检查,防止原型污染或命令注入。
- [ ] 测试了 CSP 和沙箱下的功能完整性,确保不会影响正常业务。
远程页面的安全性不是一个静态配置,而是需要在每次加载内容时都保持警觉。记住 Electron 中的远程内容本质上就是坐在你家客厅里的“客人”,把房门钥匙收好,只告诉他想用的杯碗在哪,他就不可能跑进卧室乱翻抽屉。