安全
有关如何正确披露 Electron 漏洞的信息,请参阅 SECURITY.md。
对于上游 Chromium 漏洞:Electron 会与交替的 Chromium 版本保持同步。有关更多信息,请参阅 Electron 发布时间线 文档。
前言
作为 Web 开发者,我们通常享受浏览器强大的安全保障——我们编写的代码相关的风险相对较小。我们的网站被授予有限的权限,在一个沙盒中运行,并且我们信任我们的用户享受由大型工程师团队构建的浏览器,该浏览器能够快速响应新发现的安全威胁。
在使用 Electron 时,重要的是要理解 Electron 不是一个 Web 浏览器。它允许您使用熟悉的 Web 技术构建功能丰富的桌面应用程序,但您的代码拥有更大的权限。JavaScript 可以访问文件系统、用户 shell 等。这使您可以构建高质量的本机应用程序,但固有的安全风险随着授予代码的额外权限而增加。
考虑到这一点,请注意,从不受信任的来源显示任意内容会带来严重的安全性风险,Electron 并非设计用于处理这种情况。事实上,最流行的 Electron 应用程序(Atom、Slack、Visual Studio Code 等)主要显示本地内容(或受信任、安全的远程内容,不使用 Node 集成)——如果您的应用程序从在线来源执行代码,您有责任确保该代码不是恶意的。
通用指南
安全是每个人的责任
重要的是要记住,您的 Electron 应用程序的安全性是框架基础(Chromium、Node.js)、Electron 本身、所有 NPM 依赖项以及您的代码的整体安全性的结果。因此,您有责任遵循一些重要的最佳实践
-
保持您的应用程序与最新的 Electron 框架版本保持更新。 发布您的产品时,您也在发布一个由 Electron、Chromium 共享库和 Node.js 组成的捆绑包。影响这些组件的漏洞可能会影响您应用程序的安全性。通过将 Electron 更新到最新版本,您可以确保关键漏洞(例如nodeIntegration 绕过)已修补,并且无法在您的应用程序中利用。有关更多信息,请参阅 "使用最新版本的 Electron"。
-
评估您的依赖项。 虽然 NPM 提供了五十万个可重用的包,但选择受信任的第三方库是您的责任。如果您使用受已知漏洞影响的过时库或依赖于维护不良的代码,您的应用程序安全可能会受到威胁。
-
采用安全的编码实践。 您的应用程序的第一道防线是您自己的代码。常见的 Web 漏洞,例如跨站脚本攻击 (XSS),对 Electron 应用程序具有更高的安全影响,因此强烈建议您采用安全的软件开发最佳实践并执行安全测试。
隔离不受信任的内容
每当您从不受信任的来源(例如远程服务器)接收代码并在本地执行它时,就会存在安全问题。例如,考虑在默认 BrowserWindow 中显示的远程网站。如果攻击者设法更改了所述内容(无论是直接攻击来源,还是坐在您的应用程序和实际目的地之间),他们将能够对用户的机器上执行本机代码。
在任何情况下,都不应在启用 Node.js 集成的情况下加载和执行远程代码。相反,仅使用与您的应用程序打包在一起的本地文件来执行 Node.js 代码。要显示远程内容,请使用 <webview> 标签或 WebContentsView,并确保禁用 nodeIntegration 并启用 contextIsolation。
安全警告和建议会打印到开发者控制台。它们仅在二进制文件的名称为 Electron 时显示,表明开发者当前正在查看控制台。
您可以通过在 process.env 或 window 对象上设置 ELECTRON_ENABLE_SECURITY_WARNINGS 或 ELECTRON_DISABLE_SECURITY_WARNINGS 来强制启用或强制禁用这些警告。
检查清单:安全建议
您至少应该遵循以下步骤来提高应用程序的安全性
- 仅加载安全内容
- 不要为远程内容启用 Node.js 集成
- 在所有渲染器中启用上下文隔离
- 启用进程沙箱
- 在加载远程内容的所有会话中使用
ses.setPermissionRequestHandler() - 不要禁用
webSecurity - 定义
Content-Security-Policy并使用限制性规则(例如script-src 'self') - 不要启用
allowRunningInsecureContent - 不要启用实验性功能
- 不要使用
enableBlinkFeatures <webview>:不要使用allowpopups<webview>:验证选项和参数- 禁用或限制导航
- 禁用或限制创建新窗口
- 不要使用
shell.openExternal处理不受信任的内容 - 使用最新版本的 Electron
- 验证所有 IPC 消息的
sender - 避免使用
file://协议,而更喜欢使用自定义协议 - 检查您可以更改的熔丝
- 不要将 Electron API 暴露给不受信任的 Web 内容
1. 仅加载安全内容
任何未包含在您的应用程序中的资源都应使用安全的协议(如 HTTPS)加载。换句话说,不要使用不安全的协议,如 HTTP。 同样,我们建议使用 WSS 代替 WS、FTPS 代替 FTP,等等。
为什么?
HTTPS 有两个主要好处
- 它确保数据完整性,断言数据在您的应用程序和主机之间传输时未被修改。
- 它加密用户和目标主机之间的流量,从而使窃听您应用程序和主机之间发送的信息更加困难。
如何?
// Bad
browserWindow.loadURL('http://example.com')
// Good
browserWindow.loadURL('https://example.com')
<!-- 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 之后的默认行为。
至关重要的是,不要在加载远程内容的任何渲染器(BrowserWindow、WebContentsView 或 <webview>)中启用 Node.js 集成。目标是限制您授予远程内容的权限,从而使攻击者更难在用户计算机上造成危害,即使他们能够执行您网站上的 JavaScript。
之后,您可以为特定主机授予额外的权限。例如,如果您正在打开一个指向 https://example.com/ 的 BrowserWindow,您可以为该网站提供它需要的确切权限,但不能更多。
为什么?
如果攻击者可以跳出渲染进程并在用户计算机上执行代码,则跨站脚本攻击 (XSS) 攻击会更危险。跨站脚本攻击相当常见——虽然这是一个问题,但它们的威力通常仅限于破坏它们执行的网站。禁用 Node.js 集成有助于防止 XSS 升级为所谓的“远程代码执行”(RCE) 攻击。
如何?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
})
mainWindow.loadURL('https://example.com')
// Good
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(app.getAppPath(), 'preload.js')
}
})
mainWindow.loadURL('https://example.com')
<!-- 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 的 Content Scripts 相同技术来启用此行为。
即使使用 nodeIntegration: false,为了真正强制执行强大的隔离并防止使用 Node 原语,也必须使用 contextIsolation。
请注意,禁用上下文隔离(参见上方)也会禁用进程沙箱。请参阅下文。
有关 contextIsolation 是什么以及如何启用它的更多信息,请参阅我们的专用 上下文隔离 文档。
4. 启用进程沙箱
这是 Electron 20.0.0 之后的默认行为。
此外,可以为所有渲染器进程应用程序范围强制执行进程沙箱:全局启用沙箱
禁用上下文隔离(参见上方)也会禁用进程沙箱,无论默认设置、sandbox: false 或全局启用的沙箱如何!
沙箱 是 Chromium 的一项功能,它使用操作系统显着限制渲染进程可以访问的内容。您应该在所有渲染器中启用沙箱。加载、读取或处理任何不受信任的内容,包括主进程,而不建议使用未沙箱化的进程。
有关进程沙箱是什么以及如何启用它的更多信息,请参阅我们的专用 进程沙箱 文档。
5. 处理来自远程内容的会话权限请求
在使用 Chrome 时,您可能见过权限请求:每当网站尝试使用用户必须手动批准的功能时(例如通知),它们就会弹出。
该 API 基于 Chromium 权限 API,并实现了相同类型的权限。
为什么?
默认情况下,Electron 将自动批准所有权限请求,除非开发者手动配置了自定义处理程序。虽然这是一个可靠的默认设置,但注重安全的开发者可能希望采取完全相反的做法。
如何操作?
const { session } = require('electron')
const { URL } = require('node:url')
session
.defaultSession
.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)
}
})
注意:session.defaultSession 仅在调用 app.whenReady 后可用。
6. 不要禁用 webSecurity
这是 Electron 的默认推荐设置。
您可能已经猜到,在渲染进程(BrowserWindow、WebContentsView 或 <webview>)上禁用 webSecurity 属性会禁用关键的安全功能。
不要在生产应用程序中禁用 webSecurity。
为什么?
禁用 webSecurity 将禁用同源策略,并将 allowRunningInsecureContent 属性设置为 true。换句话说,它允许从不同域执行不安全代码。
如何操作?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false
}
})
// Good
const mainWindow = new BrowserWindow()
<!-- 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 处理程序设置。
const { session } = require('electron')
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src \'none\'']
}
})
})
注意:session.defaultSession 仅在调用 app.whenReady 后可用。
CSP meta 标签
CSP 的首选传递机制是 HTTP 标头。但是,在使用 file:// 协议加载资源时,无法使用此方法。在某些情况下,在标记中使用 <meta> 标签直接在页面上设置策略会很有用。
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
8. 不要启用 allowRunningInsecureContent
这是 Electron 的默认推荐设置。
默认情况下,Electron 不允许通过 HTTPS 加载的网站加载和执行来自不安全来源(HTTP)的脚本、CSS 或插件。将 allowRunningInsecureContent 属性设置为 true 会禁用该保护。
通过 HTTPS 加载网站的初始 HTML,并尝试通过 HTTP 加载后续资源也被称为“混合内容”。
为什么?
通过 HTTPS 加载内容可以确保加载资源的真实性和完整性,同时加密流量本身。有关更多详细信息,请参阅关于 仅显示安全内容 的部分。
如何操作?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
// Good
const mainWindow = new BrowserWindow({})
9. 不要启用实验性功能
这是 Electron 的默认推荐设置。
Electron 的高级用户可以使用 experimentalFeatures 属性启用实验性的 Chromium 功能。
为什么?
实验性功能顾名思义是实验性的,尚未为所有 Chromium 用户启用。此外,它们对 Electron 的整体影响可能尚未经过测试。
存在合法的用例,但除非您知道自己在做什么,否则不应启用此属性。
如何操作?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
// Good
const mainWindow = new BrowserWindow({})
10. 不要使用 enableBlinkFeatures
这是 Electron 的默认推荐设置。
Blink 是 Chromium 背后的渲染引擎的名称。与 experimentalFeatures 类似,enableBlinkFeatures 属性允许开发者启用默认情况下已被禁用的功能。
为什么?
通常,如果某个功能默认情况下未启用,则可能有充分的理由。启用特定功能存在合法的用例。作为开发者,您应该确切知道为什么需要启用某个功能,其影响是什么,以及它如何影响应用程序的安全性。在任何情况下,都不应推测性地启用功能。
如何操作?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
// Good
const mainWindow = new BrowserWindow()
11. 不要为 WebViews 使用 allowpopups
这是 Electron 的默认推荐设置。
如果您正在使用 <webview>,您可能需要加载到 <webview> 标签中的页面和脚本来打开新窗口。allowpopups 属性使它们能够使用 window.open() 方法创建新的 BrowserWindows。否则,<webview> 标签不允许创建新窗口。
为什么?
如果您不需要弹出窗口,那么最好默认情况下不允许创建新的 BrowserWindows。这遵循最小权限原则:除非您知道网站需要该功能,否则不要让网站创建新的弹出窗口。
如何操作?
<!-- Bad -->
<webview allowpopups src="page.html"></webview>
<!-- Good -->
<webview src="page.html"></webview>
12. 在创建之前验证 WebView 选项
在渲染进程中创建的 WebView,如果未启用 Node.js 集成,将无法自行启用集成。但是,WebView 始终会创建一个具有自己的 webPreferences 的独立渲染进程。
从主进程控制新 <webview> 标签的创建,并验证其 webPreferences 是否没有禁用安全功能,是一个好主意。
为什么?
由于 <webview> 存在于 DOM 中,因此即使 Node.js 集成被禁用,也可以通过在您的网站上运行的脚本创建它们。
Electron 允许开发者禁用控制渲染进程的各种安全功能。在大多数情况下,开发者不需要禁用任何这些功能 - 因此,您不应允许为新创建的 <webview> 标签设置不同的配置。
如何操作?
在附加 <webview> 标签之前,Electron 会在托管 webContents 上触发 will-attach-webview 事件。使用该事件来防止创建具有可能不安全的选项的 webViews。
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. 禁用或限制导航
如果您的应用程序不需要导航或只需要导航到已知页面,那么最好完全限制导航到该已知范围,禁止任何其他类型的导航。
为什么?
导航是一种常见的攻击媒介。如果攻击者能够说服您的应用程序导航到当前页面之外,他们可能会强制您的应用程序在 Internet 上打开网站。即使您的 webContents 配置为更安全(例如禁用 nodeIntegration 或启用 contextIsolation),让您的应用程序打开随机网站也会使利用您的应用程序的工作变得更容易。
一种常见的攻击模式是,攻击者说服您的应用程序的用户以某种方式与应用程序交互,从而使其导航到攻击者的页面之一。通常通过链接、插件或其他用户生成的内容来完成。
如何做?
如果你的应用不需要导航,你可以在 will-navigate 处理程序中调用 event.preventDefault()。如果你知道你的应用可能会导航到哪些页面,请在事件处理程序中检查 URL,并且仅当它与你期望的 URL 匹配时才允许导航发生。
我们建议你使用 Node 的 URL 解析器。简单的字符串比较有时可能会被欺骗——一个 startsWith('https://example.com') 测试会允许 https://example.com.attacker.com 通过。
const { app } = require('electron')
const { URL } = require('node:url')
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 以及用于创建它的选项。我们建议你注册一个处理程序来监视窗口的创建,并拒绝任何意外的窗口创建。
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 可能会被利用来破坏用户的宿主。当 openExternal 与不受信任的内容一起使用时,它可以被利用来执行任意命令。
如何做?
// Bad
const { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
// 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 和子窗口。如果你有一个 IPC 消息,它通过 event.reply 将用户数据返回给发送者,或者执行渲染器无法原生执行的特权操作,你应该确保你没有监听来自第三方 Web 框架的消息。
你应该默认验证 所有 IPC 消息的 sender。
如何做?
// 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 版本,可以使用 熔丝 来打开或关闭这些选项。
为什么?
一些熔丝,例如 runAsNode 和 nodeCliInspect,允许应用程序在从命令行使用特定环境变量或 CLI 参数运行时表现不同。这些可用于通过你的应用程序在设备上执行命令。
这可以让外部脚本运行它们可能无权运行的命令,但你的应用程序可能有权运行这些命令。
如何做?
@electron/fuses 是我们制作的一个模块,用于轻松切换这些熔丝。请查看该模块的 README,了解更多关于用法和潜在错误情况的详细信息,并参考我们的文档中的 如何切换熔丝?。
20. 不要将 Electron API 暴露给不受信任的 Web 内容
你不应该直接将 Electron 的 API,尤其是 IPC,暴露给预加载脚本中的不受信任的 Web 内容。
为什么?
暴露原始 API,例如 ipcRenderer.on,是危险的,因为它使渲染进程可以直接访问整个 IPC 事件系统,允许它们监听任何 IPC 事件,而不仅仅是为它们设计的事件。
为了避免这种暴露,我们也不能直接传递回调:IPC 事件回调的第一个参数是 IpcRendererEvent 对象,它包括 sender 等属性,这些属性提供对底层 ipcRenderer 实例的访问。即使你只监听特定事件,直接传递回调也意味着渲染器可以访问此事件对象。
简而言之,我们希望不受信任的 Web 内容只能访问必要的信息和 API。
如何做?
// Bad
contextBridge.exposeInMainWorld('electronAPI', {
on: ipcRenderer.on
})
// Also bad
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
// Good
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})
有关 contextIsolation 是什么以及如何使用它来保护你的应用程序的更多信息,请参阅 上下文隔离 文档。