Electron 安全性

上下文隔离、沙箱、CSP、权限校验,以及最常见的四类漏洞。

Advanced
Key PointsElectron 的安全边界只有一条:页面不可信。凡是页面能传过来的东西,主进程都必须校验。

默认配置就是安全配置

JavaScript
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.

四类常见漏洞

RiskHowFix
XSS → RCE页面注入脚本,若能摸到 Node 就能执行任意命令隔离 + 沙箱 + CSP
路径穿越IPC 传 ../../etc/passwd 读文件path.resolve 后校验前缀
任意导航页面跳到恶意站点,继承预加载脚本will-navigate 拦白名单
外链滥用target=_blank 打开任意站点setWindowOpenHandler 限制

CSP

HTML
<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.

拦截导航与新窗口

JavaScript
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 参数校验

JavaScript
// 不好:什么都信
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.

只暴露需要的函数

JavaScript
// 危险:把整个 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)
});
Exposing a generic 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.

检查清单

  1. contextIsolation: true、nodeIntegration: false、sandbox: true
  2. 页面里配 CSP,生产环境不出现 unsafe-eval
  3. 所有 IPC 参数做类型与范围校验
  4. will-navigate 与 setWindowOpenHandler 都拦
  5. 预加载只暴露具体函数,不暴露通用通道
  6. 不用 shell.openExternal 打开未经校验的 URL

Friends & Partners