Electron 安全性
上下文隔离、沙箱、CSP、权限校验,以及最常见的四类漏洞。
默认配置就是安全配置
new BrowserWindow({
webPreferences: {
contextIsolation: true, // 默认 true
nodeIntegration: false, // 默认 false
sandbox: true, // 默认 true
webSecurity: true, // 默认 true
allowRunningInsecureContent: false
}
});Since Electron 12 these defaults are the secure ones. Copying nodeIntegration: true from an old tutorial now means leaving the door open.
四类常见漏洞
| Risk | How | Fix |
|---|---|---|
| XSS → RCE | 页面注入脚本,若能摸到 Node 就能执行任意命令 | 隔离 + 沙箱 + CSP |
| 路径穿越 | IPC 传 ../../etc/passwd 读文件 | path.resolve 后校验前缀 |
| 任意导航 | 页面跳到恶意站点,继承预加载脚本 | will-navigate 拦白名单 |
| 外链滥用 | target=_blank 打开任意站点 | setWindowOpenHandler 限制 |
CSP
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.example.com">script-src 'self' blocks inline scripts and eval — the most effective wall against XSS. Vue/React dev builds need unsafe-eval, so tighten only in production builds.
拦截导航与新窗口
const { shell } = require('electron');
// 主窗口只允许留在自己的页面里
win.webContents.on('will-navigate', (e, url) => {
if (!url.startsWith('file://') && !url.startsWith('https://app.example.com')) {
e.preventDefault();
shell.openExternal(url); // 外链交给系统浏览器
}
});
// 页面里的 window.open / target=_blank
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://')) shell.openExternal(url);
return { action: 'deny' }; // 一律不开新窗口
});Without this, an injected link can navigate the whole app to a phishing site that still carries the preload bridge — handing the IPC surface to an attacker.
IPC 参数校验
// 不好:什么都信
ipcMain.handle('read', (_e, p) => fs.readFile(p, 'utf8'));
// 好:白名单 + 路径校验 + 类型校验
const ROOT = path.join(app.getPath('userData'), 'files');
ipcMain.handle('file:read', async (_e, name) => {
if (typeof name !== 'string' || !/^[\w.-]+$/.test(name)) {
throw new Error('非法文件名');
}
const target = path.resolve(ROOT, name);
if (!target.startsWith(ROOT + path.sep)) {
throw new Error('路径越界');
}
return fs.readFile(target, 'utf8');
});The event object carries sender, letting you check who is calling. In a multi-window app where windows have different privileges, verify the identity of event.senderFrame.
只暴露需要的函数
// 危险:把整个 ipcRenderer 暴露出去
contextBridge.exposeInMainWorld('ipc', ipcRenderer);
// 危险:暴露一个通用通道
contextBridge.exposeInMainWorld('api', {
invoke: (ch, ...args) => ipcRenderer.invoke(ch, ...args)
});
// 安全:一个功能一个函数
contextBridge.exposeInMainWorld('api', {
loadSettings: () => ipcRenderer.invoke('settings:load'),
saveSettings: (s) => ipcRenderer.invoke('settings:save', s)
});invoke hands the page every channel in the main process, not just settings:save. This is the single most frequent finding in Electron security audits.检查清单
contextIsolation: true、nodeIntegration: false、sandbox: true- 页面里配 CSP,生产环境不出现
unsafe-eval - 所有 IPC 参数做类型与范围校验
will-navigate与setWindowOpenHandler都拦- 预加载只暴露具体函数,不暴露通用通道
- 不用
shell.openExternal打开未经校验的 URL
默认配置就是安全配置
new BrowserWindow({
webPreferences: {
contextIsolation: true, // 默认 true
nodeIntegration: false, // 默认 false
sandbox: true, // 默认 true
webSecurity: true, // 默认 true
allowRunningInsecureContent: false
}
});Electron 从 12 版起把这些默认值改成了安全值。老教程里那些 nodeIntegration: true 的写法,现在照抄就是把大门敞开。
四类常见漏洞
| 风险 | 怎么发生 | 怎么防 |
|---|---|---|
| XSS 到命令执行 | 页面注入脚本,若能摸到 Node 就能执行任意命令 | 隔离 + 沙箱 + CSP |
| 路径穿越 | IPC 传 ../../etc/passwd 读文件 | path.resolve 后校验前缀 |
| 任意导航 | 页面跳到恶意站点,继承预加载脚本 | will-navigate 拦白名单 |
| 外链滥用 | target=_blank 打开任意站点 | setWindowOpenHandler 限制 |
CSP
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.example.com">script-src 'self' 禁止内联脚本和 eval,这是挡住 XSS 最有效的一道墙。用 Vue/React 的开发版时它需要 unsafe-eval,所以只在生产构建里收紧。
拦截导航与新窗口
const { shell } = require('electron');
// 主窗口只允许留在自己的页面里
win.webContents.on('will-navigate', (e, url) => {
if (!url.startsWith('file://') && !url.startsWith('https://app.example.com')) {
e.preventDefault();
shell.openExternal(url); // 外链交给系统浏览器
}
});
// 页面里的 window.open / target=_blank
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://')) shell.openExternal(url);
return { action: 'deny' }; // 一律不开新窗口
});不拦的话,页面里一个被注入的链接就能把整个应用导航到钓鱼站点,而那个站点仍带着预加载脚本的桥——等于把 IPC 接口交给了攻击者。
IPC 参数校验
// 不好:什么都信
ipcMain.handle('read', (_e, p) => fs.readFile(p, 'utf8'));
// 好:白名单 + 路径校验 + 类型校验
const ROOT = path.join(app.getPath('userData'), 'files');
ipcMain.handle('file:read', async (_e, name) => {
if (typeof name !== 'string' || !/^[\w.-]+$/.test(name)) {
throw new Error('非法文件名');
}
const target = path.resolve(ROOT, name);
if (!target.startsWith(ROOT + path.sep)) {
throw new Error('路径越界');
}
return fs.readFile(target, 'utf8');
});_e 这个事件对象带着 sender,可以用来校验「是谁在调用」。多窗口应用里如果不同窗口权限不同,必须查 event.senderFrame 对应的窗口身份。
只暴露需要的函数
// 危险:把整个 ipcRenderer 暴露出去
contextBridge.exposeInMainWorld('ipc', ipcRenderer);
// 危险:暴露一个通用通道
contextBridge.exposeInMainWorld('api', {
invoke: (ch, ...args) => ipcRenderer.invoke(ch, ...args)
});
// 安全:一个功能一个函数
contextBridge.exposeInMainWorld('api', {
loadSettings: () => ipcRenderer.invoke('settings:load'),
saveSettings: (s) => ipcRenderer.invoke('settings:save', s)
});invoke 等于把主进程所有通道都给了页面——settings:save 之外的每个 handler 都能被调。这是 Electron 安全审计里出现频率最高的一条。检查清单
contextIsolation: true、nodeIntegration: false、sandbox: true- 页面里配 CSP,生产环境不出现
unsafe-eval - 所有 IPC 参数做类型与范围校验
will-navigate与setWindowOpenHandler都拦- 预加载只暴露具体函数,不暴露通用通道
- 不用
shell.openExternal打开未经校验的 URL