跳至主要内容

安全

报告安全问题

有关如何正确披露 Electron 漏洞的信息,请参阅 SECURITY.md

对于上游 Chromium 漏洞:Electron 会与交替的 Chromium 版本保持同步。有关更多信息,请参阅 Electron 版本时间表 文档。

前言

作为 Web 开发人员,我们通常享受浏览器强大的安全网 — 我们编写的代码相关的风险相对较小。我们的网站在沙盒中被授予有限的权限,我们相信我们的用户享受着由一支能够快速响应新发现的安全威胁的大型工程师团队构建的浏览器。

在使用 Electron 时,了解 Electron 不是 Web 浏览器非常重要。它允许你使用熟悉的 Web 技术构建功能丰富的桌面应用程序,但你的代码拥有更大的权限。JavaScript 可以访问文件系统、用户 shell 等。这允许你构建高质量的原生应用程序,但固有的安全风险会随着授予你的代码的附加权限而扩大。

考虑到这一点,请注意,显示来自不受信任来源的任意内容会带来严重的安全性风险,而 Electron 并不打算处理此类风险。事实上,最流行的 Electron 应用程序(Atom、Slack、Visual Studio Code 等)主要显示本地内容(或受信任、安全的远程内容,不含 Node 集成)——如果您的应用程序执行来自在线来源的代码,您有责任确保该代码没有恶意。

一般准则

安全性是每个人的责任

务必记住,Electron 应用程序的安全性取决于框架基础(ChromiumNode.js)、Electron 本身、所有 NPM 依赖项和您的代码的整体安全性。因此,您有责任遵循一些重要的最佳实践

  • 使用最新的 Electron 框架版本保持应用程序的最新状态。发布产品时,您还会发布一个由 Electron、Chromium 共享库和 Node.js 组成的捆绑包。影响这些组件的漏洞可能会影响您应用程序的安全性。通过将 Electron 更新到最新版本,您可以确保已修补关键漏洞(例如nodeIntegration 绕过),并且无法在您的应用程序中利用这些漏洞。有关更多信息,请参阅“使用当前版本的 Electron”。

  • 评估您的依赖项。虽然 NPM 提供了五十万个可重复使用的包,但选择受信任的第三方库是您的责任。如果您使用受已知漏洞影响的过时库或依赖维护不善的代码,您的应用程序安全性可能会面临危险。

  • 采用安全的编码实践。应用程序的第一道防线是您自己的代码。常见的 Web 漏洞(例如跨站点脚本 (XSS))对 Electron 应用程序的安全性影响更大,因此强烈建议采用安全的软件开发最佳实践并执行安全性测试。

隔离不受信任的内容

每当您从不受信任的来源(例如远程服务器)接收代码并在本地执行代码时,就会出现安全问题。例如,考虑在默认 BrowserWindow 中显示远程网站。如果攻击者设法以某种方式更改所述内容(通过直接攻击源或介于您的应用程序和实际目标之间),他们将能够在用户的计算机上执行本机代码。

危险

在任何情况下,都不应在启用 Node.js 集成的情况下加载和执行远程代码。相反,仅使用本地文件(与应用程序一起打包)来执行 Node.js 代码。要显示远程内容,请使用 <webview> 标签或 BrowserView,确保禁用 nodeIntegration 并启用 contextIsolation

Electron 安全警告

安全警告和建议会打印到开发者控制台。它们仅在二进制文件的名称为 Electron 时显示,表明开发人员当前正在查看控制台。

您可以通过在 process.envwindow 对象上设置 ELECTRON_ENABLE_SECURITY_WARNINGSELECTRON_DISABLE_SECURITY_WARNINGS 来强制启用或强制禁用这些警告。

清单:安全建议

您至少应遵循以下步骤来提高应用程序的安全性

  1. 仅加载安全内容
  2. 禁用显示远程内容的所有渲染器中的 Node.js 集成
  3. 在所有渲染器中启用上下文隔离
  4. 启用进程沙箱
  5. 在加载远程内容的所有会话中使用 ses.setPermissionRequestHandler()
  6. 不要禁用 webSecurity
  7. 定义 Content-Security-Policy 并使用限制性规则(即 script-src 'self'
  8. 不要启用 allowRunningInsecureContent
  9. 不要启用实验性功能
  10. 不要使用 enableBlinkFeatures
  11. <webview>:不要使用 allowpopups
  12. <webview>:验证选项和参数
  13. 禁用或限制导航
  14. 禁用或限制创建新窗口
  15. 不要对不受信任的内容使用 shell.openExternal
  16. 使用当前版本的 Electron
  17. 验证所有 IPC 消息的 sender
  18. 避免使用 file:// 协议,优先使用自定义协议
  19. 检查您可以更换哪些保险丝

若要自动检测错误配置和不安全的模式,可以使用 Electronegativity。有关使用 Electron 开发应用程序时的潜在弱点和实现错误的更多详细信息,请参阅此 开发人员和审计人员指南

1. 仅加载安全内容

不应使用您的应用程序附带的任何资源,而应使用安全协议(如 HTTPS)进行加载。换句话说,不要使用不安全的协议,如 HTTP。同样,我们建议使用 WSS 代替 WS、使用 FTPS 代替 FTP,依此类推。

为什么?

HTTPS 有两个主要优点

  1. 它确保数据完整性,断言数据在您的应用程序和主机之间传输时未被修改。
  2. 它对您用户和目标主机之间的流量进行加密,使得更难以窃听您的应用程序和主机之间发送的信息。

如何?

main.js(主进程)
// Bad
browserWindow.loadURL('http://example.com')

// Good
browserWindow.loadURL('https://example.com')
index.html(渲染器进程)
<!-- Bad -->
<script crossorigin src="http://example.com/react.js"></script>
<link rel="stylesheet" href="http://example.com/style.css">

<!-- Good -->
<script crossorigin src="https://example.com/react.js"></script>
<link rel="stylesheet" href="https://example.com/style.css">

2. 不要为远程内容启用 Node.js 集成

信息

此建议是 Electron 5.0.0 中的默认行为。

至关重要的是,您不要在加载远程内容的任何渲染器(BrowserWindowBrowserView<webview>)中启用 Node.js 集成。目标是限制您授予远程内容的权限,从而大幅增加攻击者在您的网站上执行 JavaScript 时伤害您用户的难度。

在此之后,你可以为特定主机授予其他权限。例如,如果你正在打开指向 `https://example.com/` 的 BrowserWindow,你可以为该网站提供它需要的权限,但不能更多。

为什么?

如果攻击者可以跳出渲染器进程并在用户的计算机上执行代码,那么跨站点脚本 (XSS) 攻击会更加危险。跨站点脚本攻击相当常见——虽然是一个问题,但它们的力量通常仅限于破坏它们执行的网站。禁用 Node.js 集成有助于防止 XSS 升级为所谓的“远程代码执行”(RCE) 攻击。

如何?

main.js(主进程)
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
})

mainWindow.loadURL('https://example.com')
main.js(主进程)
// Good
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(app.getAppPath(), 'preload.js')
}
})

mainWindow.loadURL('https://example.com')
index.html(渲染器进程)
<!-- Bad -->
<webview nodeIntegration src="page.html"></webview>

<!-- Good -->
<webview src="page.html"></webview>

在禁用 Node.js 集成时,你仍然可以向你的网站公开使用 Node.js 模块或特性的 API。预加载脚本继续访问 `require` 和其他 Node.js 特性,允许开发人员通过 contextBridge API 向远程加载的内容公开自定义 API。

3. 启用上下文隔离

信息

此建议是 Electron 在 12.0.0 中的默认行为。

上下文隔离是一个 Electron 特性,它允许开发人员在预加载脚本和 Electron API 中在一个专门的 JavaScript 上下文中运行代码。实际上,这意味着全局对象(如 `Array.prototype.push` 或 `JSON.parse`)不能被在渲染器进程中运行的脚本修改。

Electron 使用与 Chromium 的 内容脚本 相同的技术来启用此行为。

即使使用了 `nodeIntegration: false`,为了真正强制执行强隔离并防止使用 Node 原语,必须同时使用 `contextIsolation`。

信息

有关 `contextIsolation` 是什么以及如何启用它的更多信息,请参阅我们专门的 上下文隔离 文档。

4. 启用进程沙盒

沙盒 是一项 Chromium 特性,它使用操作系统来显著限制渲染器进程可以访问的内容。你应该在所有渲染器中启用沙盒。不建议在未沙盒化的进程(包括主进程)中加载、读取或处理任何不受信任的内容。

信息

有关进程沙盒是什么以及如何启用它的更多信息,请参阅我们专门的 进程沙盒 文档。

5. 处理来自远程内容的会话权限请求

使用 Chrome 时,你可能看到过权限请求:当网站尝试使用用户必须手动批准的功能(例如通知)时,它们会弹出。

该 API 基于 Chromium 权限 API,并实现了相同类型的权限。

为什么?

默认情况下,除非开发人员手动配置自定义处理程序,否则 Electron 将自动批准所有权限请求。虽然这是一个可靠的默认设置,但注重安全的开发人员可能希望采取完全相反的做法。

如何?

main.js(主进程)
const { session } = require('electron')
const { URL } = require('url')

session
.fromPartition('some-partition')
.setPermissionRequestHandler((webContents, permission, callback) => {
const parsedUrl = new URL(webContents.getURL())

if (permission === 'notifications') {
// Approves the permissions request
callback(true)
}

// Verify URL
if (parsedUrl.protocol !== 'https:' || parsedUrl.host !== 'example.com') {
// Denies the permissions request
return callback(false)
}
})

6. 不要禁用 webSecurity

信息

此建议是 Electron 的默认设置。

你可能已经猜到,在渲染器进程(BrowserWindowBrowserView<webview>)上禁用 webSecurity 属性会禁用至关重要的安全功能。

不要在生产应用程序中禁用 webSecurity

为什么?

禁用 webSecurity 将禁用同源策略,并将 allowRunningInsecureContent 属性设置为 true。换句话说,它允许执行来自不同域的不安全代码。

如何?

main.js(主进程)
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false
}
})
main.js(主进程)
// Good
const mainWindow = new BrowserWindow()
index.html(渲染器进程)
<!-- Bad -->
<webview disablewebsecurity src="page.html"></webview>

<!-- Good -->
<webview src="page.html"></webview>

7. 定义内容安全策略

内容安全策略 (CSP) 是针对跨站点脚本攻击和数据注入攻击的额外保护层。我们建议在 Electron 中加载的任何网站都启用它们。

为什么?

CSP 允许提供内容的服务器限制和控制 Electron 可以为给定网页加载的资源。应该允许 https://example.com 从你定义的源加载脚本,而来自 https://evil.attacker.com 的脚本不应该被允许运行。定义 CSP 是提高应用程序安全性的简单方法。

如何?

以下 CSP 将允许 Electron 从当前网站和 `apis.example.com` 执行脚本。

// Bad
Content-Security-Policy: '*'

// Good
Content-Security-Policy: script-src 'self' https://apis.example.com

CSP HTTP 标头

Electron 尊重 Content-Security-Policy HTTP 标头,该标头可以使用 Electron 的 webRequest.onHeadersReceived 处理程序进行设置

main.js(主进程)
const { session } = require('electron')

session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src \'none\'']
}
})
})

CSP 元标记

CSP 首选的传递机制是 HTTP 标头。但是,在使用 `file://` 协议加载资源时无法使用此方法。在某些情况下,使用 `<meta>` 标记直接在标记中为页面设置策略可能很有用

index.html(渲染器进程)
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">

8. 不要启用 `allowRunningInsecureContent`

信息

此建议是 Electron 的默认设置。

默认情况下,Electron 不会允许通过 `HTTPS` 加载的网站从不安全来源(`HTTP`)加载和执行脚本、CSS 或插件。将属性 `allowRunningInsecureContent` 设置为 `true` 会禁用该保护。

通过 `HTTPS` 加载网站的初始 HTML 并尝试通过 `HTTP` 加载后续资源也称为“混合内容”。

为什么?

通过 `HTTPS` 加载内容可确保加载资源的真实性和完整性,同时加密流量本身。有关更多详细信息,请参阅 仅显示安全内容 部分。

如何?

main.js(主进程)
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
main.js(主进程)
// Good
const mainWindow = new BrowserWindow({})

9. 不要启用实验性功能

信息

此建议是 Electron 的默认设置。

Electron 的高级用户可以使用 `experimentalFeatures` 属性启用实验性 Chromium 功能。

为什么?

顾名思义,实验性功能是实验性的,尚未向所有 Chromium 用户启用。此外,它们对 Electron 整体的影响可能尚未经过测试。

存在合法的用例,但除非您知道自己在做什么,否则不应启用此属性。

如何?

main.js(主进程)
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
main.js(主进程)
// Good
const mainWindow = new BrowserWindow({})

10. 不要使用 enableBlinkFeatures

信息

此建议是 Electron 的默认设置。

Blink 是 Chromium 背后的渲染引擎的名称。与 experimentalFeatures 一样,enableBlinkFeatures 属性允许开发者启用默认情况下已禁用的功能。

为什么?

一般来说,如果某个功能默认情况下未启用,那么可能是有充分理由的。存在启用特定功能的合法用例。作为一名开发者,您应该确切地知道为什么需要启用某个功能、启用后的影响以及它如何影响应用程序的安全性。在任何情况下,您都不应该推测性地启用功能。

如何?

main.js(主进程)
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
main.js(主进程)
// Good
const mainWindow = new BrowserWindow()

11. 不要对 WebViews 使用 allowpopups

信息

此建议是 Electron 的默认设置。

如果您正在使用 <webview>,您可能需要在 <webview> 标签中加载的页面和脚本打开新窗口。allowpopups 属性允许它们使用 window.open() 方法创建新的 BrowserWindows。否则,不允许 <webview> 标签创建新窗口。

为什么?

如果您不需要弹出窗口,那么最好默认情况下不允许创建新的 BrowserWindows。这遵循最小所需访问权限的原则:不要让网站创建新的弹出窗口,除非您知道它需要该功能。

如何?

index.html(渲染器进程)
<!-- Bad -->
<webview allowpopups src="page.html"></webview>

<!-- Good -->
<webview src="page.html"></webview>

12. 在创建之前验证 WebView 选项

在未启用 Node.js 集成的渲染器进程中创建的 WebView 将无法自行启用集成。但是,WebView 将始终使用其自己的 webPreferences 创建一个独立的渲染器进程。

最好从主进程控制创建新的 <webview> 标签,并验证它们的 webPreferences 不会禁用安全功能。

为什么?

由于 <webview> 存在于 DOM 中,即使禁用了 Node.js 集成,它们也可以由网站上运行的脚本创建。

Electron 允许开发者禁用控制渲染器进程的各种安全功能。在大多数情况下,开发者不需要禁用任何这些功能 - 因此你不应允许为新创建的 <webview> 标签使用不同的配置。

如何?

在附加 <webview> 标签之前,Electron 会在宿主 webContents 上触发 will-attach-webview 事件。使用该事件来阻止创建具有潜在不安全选项的 webViews

main.js(主进程)
app.on('web-contents-created', (event, contents) => {
contents.on('will-attach-webview', (event, webPreferences, params) => {
// Strip away preload scripts if unused or verify their location is legitimate
delete webPreferences.preload

// Disable Node.js integration
webPreferences.nodeIntegration = false

// Verify URL being loaded
if (!params.src.startsWith('https://example.com/')) {
event.preventDefault()
}
})
})

同样,此列表只是将风险降至最低,但并不能消除风险。如果你的目标是显示网站,浏览器将是一个更安全的选择。

13. 禁用或限制导航

如果你的应用不需要导航或只需要导航到已知页面,最好直接将导航限制在已知范围内,禁止任何其他类型的导航。

为什么?

导航是一种常见的攻击媒介。如果攻击者能够说服你的应用导航离开当前页面,他们就有可能强迫你的应用在互联网上打开网站。即使你的 webContents 被配置为更安全(如禁用 nodeIntegration 或启用 contextIsolation),让你的应用打开一个随机网站也会让攻击者更容易利用你的应用。

一种常见的攻击模式是攻击者说服你的应用用户以某种方式与应用交互,使其导航到攻击者自己的页面之一。这通常通过链接、插件或其他用户生成的内容来完成。

如何?

如果你的应用不需要导航,你可以在 will-navigate 处理程序中调用 event.preventDefault()。如果你知道你的应用可能导航到哪些页面,请在事件处理程序中检查 URL,并且仅在 URL 与你预期的 URL 匹配时才允许导航发生。

我们建议你使用 Node 的 URL 解析器。简单的字符串比较有时可能会被欺骗 - 一个 startsWith('https://example.com') 测试将允许 https://example.com.attacker.com 通过。

main.js(主进程)
const { URL } = require('url')
const { app } = require('electron')

app.on('web-contents-created', (event, contents) => {
contents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl)

if (parsedUrl.origin !== 'https://example.com') {
event.preventDefault()
}
})
})

14. 禁用或限制创建新窗口

如果你有一组已知窗口,最好限制在你的应用中创建其他窗口。

为什么?

与导航非常相似,创建新的 webContents 是一个常见的攻击媒介。攻击者试图说服你的应用创建具有比以前更多权限的新窗口、框架或其他渲染器进程;或打开以前无法打开的页面。

如果你不需要创建除了你知道你需要创建的窗口之外的窗口,那么禁用创建将免费为你提供一些额外的安全性。对于打开一个 BrowserWindow 并且不需要在运行时打开任意数量的其他窗口的应用来说,这通常是这种情况。

如何?

webContents 在创建新窗口之前会将其委托给它的 窗口打开处理程序。该处理程序将接收窗口被请求打开的 url 和用于创建它的选项等参数。我们建议你注册一个处理程序来监视窗口的创建,并拒绝任何意外的窗口创建。

main.js(主进程)
const { app, shell } = require('electron')

app.on('web-contents-created', (event, contents) => {
contents.setWindowOpenHandler(({ url }) => {
// In this example, we'll ask the operating system
// to open this event's url in the default browser.
//
// See the following item for considerations regarding what
// URLs should be allowed through to shell.openExternal.
if (isSafeForExternalOpen(url)) {
setImmediate(() => {
shell.openExternal(url)
})
}

return { action: 'deny' }
})
})

15. 不要对不受信任的内容使用 shell.openExternal

shell 模块的 openExternal API 允许使用桌面的本机实用程序打开给定的协议 URI。例如,在 macOS 上,此函数类似于 open 终端命令实用程序,并且会根据 URI 和文件类型关联打开特定应用程序。

原因?

不当使用 openExternal 可用于危害用户的 host。当 openExternal 与不受信任的内容一起使用时,可用于执行任意命令。

如何?

main.js(主进程)
//  Bad
const { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
main.js(主进程)
//  Good
const { shell } = require('electron')
shell.openExternal('https://example.com/index.html')

16. 使用最新版本的 Electron

您应始终使用最新版本的 Electron。每当发布新的主要版本时,您都应尽快尝试更新您的应用。

原因?

使用旧版 Electron、Chromium 和 Node.js 构建的应用程序比使用这些组件的较新版本的应用程序更容易成为目标。一般来说,针对旧版 Chromium 和 Node.js 的安全问题和漏洞利用更为广泛。

Chromium 和 Node.js 都是由数千名才华横溢的开发者构建的令人印象深刻的工程壮举。鉴于它们的受欢迎程度,它们的安全性经过同样熟练的安全研究人员的仔细测试和分析。其中许多研究人员负责任地披露漏洞,这通常意味着研究人员会给 Chromium 和 Node.js 一些时间来修复问题,然后再发布它们。如果您的应用程序运行的是最新版本的 Electron(因此,Chromium 和 Node.js),则您的应用程序会更安全,因为潜在的安全问题不会被广泛知晓。

如何?

一次迁移一个主要版本,同时参考 Electron 的重大更改文档,以查看是否需要更新任何代码。

17. 验证所有 IPC 消息的sender

您应始终验证传入 IPC 消息的sender属性,以确保您没有对不受信任的渲染器执行操作或向其发送信息。

原因?

理论上,所有 Web 框架都可以向主进程发送 IPC 消息,包括某些场景中的 iframe 和子窗口。如果您有通过 event.reply 向发件人返回用户数据或执行渲染器本身无法执行的特权操作的 IPC 消息,则应确保未监听第三方 Web 框架。

默认情况下,您应该验证所有 IPC 消息的 sender

如何?

main.js(主进程)
// Bad
ipcMain.handle('get-secrets', () => {
return getSecrets()
})

// Good
ipcMain.handle('get-secrets', (e) => {
if (!validateSender(e.senderFrame)) return null
return getSecrets()
})

function validateSender (frame) {
// Value the host of the URL using an actual URL parser and an allowlist
if ((new URL(frame.url)).host === 'electronjs.org') return true
return false
}

18. 避免使用 file:// 协议,优先使用自定义协议

您应该从自定义协议而不是 file:// 协议提供本地页面。

为什么?

file:// 协议在 Electron 中比在 Web 浏览器中获得更多特权,即使在浏览器中,它也与 http/https URL 的处理方式不同。使用自定义协议可以让您更符合经典 Web URL 行为,同时还能更好地控制可以加载的内容以及加载时间。

file:// 上运行的页面可以单方面访问计算机上的每个文件,这意味着 XSS 问题可用于从用户的计算机加载任意文件。使用自定义协议可以防止此类问题,因为您可以将协议限制为仅提供特定文件集。

如何?

按照 protocol.handle 示例了解如何从自定义协议提供文件/内容。

19. 检查您可以更改哪些保险丝

Electron 附带了许多选项,这些选项可能很有用,但很大一部分应用程序可能不需要。为了避免构建自己的 Electron 版本,可以使用 保险丝 来关闭或打开这些选项。

为什么?

某些保险丝(如 runAsNodenodeCliInspect)允许应用程序在使用特定环境变量或 CLI 参数从命令行运行时表现出不同的行为。这些保险丝可用于通过您的应用程序在设备上执行命令。

这可以让外部脚本运行它们可能无权运行的命令,但你的应用程序可能拥有这些权限。

如何?

我们制作了一个模块,@electron/fuses,以便轻松翻转这些熔断器。查看该模块的自述文件,了解有关用法和潜在错误情况的更多详细信息,并参阅我们文档中的如何翻转熔断器?