首页 / Electron 入门教程 / Electron 安全最佳实践

Electron 入门教程

Electron 安全最佳实践

本教程共 45 篇 · 第 39 篇 · 更新于 2026-08-03

Electron安全contextIsolationnodeIntegrationsandbox

39. Electron 安全最佳实践

本节目标

  • 理解 Electron 比浏览器更危险的根本原因。
  • 掌握官方 20 条安全清单的核心条目。
  • 弄清 contextIsolationnodeIntegrationsandbox 三者的关系。
  • 学会谨慎加载远程内容并最小化权限。
  • 知道如何在校验脚本里落实这些默认项。

1-1 为什么 Electron 的安全格外重要

浏览器为网页开发者提供了一张牢固的安全网。我们写的网页被关在沙箱里,权限很小,而且背后有一支庞大的工程师团队在快速修复新出现的漏洞。换句话说,哪怕代码出了错,破坏范围也有限。

Electron 不一样。它让你用熟悉的 Web 技术构建功能丰富的桌面应用,但你的代码拥有大得多的权力。JavaScript 可以直接读写文件系统、调用系统 shell、操作剪贴板。这种能力是把双刃剑:它让高质量的桌面应用成为可能,也让安全风险随权力一起放大。

一个关键点必须记牢:Electron 不是浏览器。当你把来自不可信来源的内容(例如远程服务器)加载进应用并执行时,就埋下了严重隐患。事实上,最流行的那些 Electron 应用,比如 VS Code、Slack、Atom,主要展示的都是本地内容,或者经过严格审查、且关闭了 Node 集成的远程内容。

提示:如果你要展示的是完全不可信的网页,浏览器本身比你的 Electron 应用更安全。Electron 适合展示可信内容,或经过隔离的远程内容。

1-2 安全清单:20 条基线

官方文档给出了一份安全清单,至少要做到下面这些条目。这里挑出最常踩坑的,逐条说明为什么以及怎么做。完整 20 条可到 Electron 官方安全文档查看。

  1. 只加载安全内容(HTTPS)。
  2. 不要为远程内容启用 Node.js 集成。
  3. 在所有渲染进程启用上下文隔离。
  4. 启用进程沙箱。
  5. 为加载远程内容的会话设置 setPermissionRequestHandler
  6. 不要禁用 webSecurity
  7. 定义严格的 Content-Security-Policy
  8. 不要启用 allowRunningInsecureContent
  9. 不要启用实验性特性。
  10. 不要使用 enableBlinkFeatures
  11. <webview> 不要使用 allowpopups
  12. 校验 <webview> 的选项与参数。
  13. 禁用或限制导航。
  14. 禁用或限制新建窗口。
  15. 不要用 shell.openExternal 打开不可信内容。
  16. 使用较新版本的 Electron。
  17. 校验所有 IPC 消息的发送方。
  18. 避免直接使用 file:// 协议,优先自定义协议。
  19. 检查可关闭的 Fuses(熔断开关)。
  20. 不要把 Electron API 暴露给不可信的 Web 内容。

下面用一段主进程代码,把最常见的几个默认项一次性设好。注意 contextIsolationnodeIntegrationsandbox 都是显式写明,而不是依赖记忆。

// 主进程 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.pushJSON.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 集成。这里的远程内容指 BrowserWindowWebContentsView<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 而非 WSFTPS 而非 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: truenodeIntegration: falsesandbox: true 作为默认,再叠加 HTTPS、CSP、权限最小化与 IPC 发送方校验。下一章我们会看几种具体的攻击手法,以及它们如何被这些默认项化解。