安全
有关如何正确披露 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 提供了 50 万个可重用软件包,但您有责任选择受信任的第三方库。如果您使用受已知漏洞影响的过时库或依赖于维护不佳的代码,则应用程序安全可能受到威胁。
-
采用安全的编码实践。应用程序的第一道防线是您自己的代码。常见的 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://
协议,并优先使用自定义协议 - 检查可以更改哪些保险丝
为了自动检测错误配置和不安全的模式,可以使用 Electronegativity。有关使用 Electron 开发应用程序时潜在弱点和实现错误的其他详细信息,请参阅此 开发人员和审计员指南。
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 集成后,您仍然可以向您的网站公开 API,这些 API 确实会使用 Node.js 模块或功能。预加载脚本继续可以访问require
和其他 Node.js 功能,允许开发人员通过contextBridge API向远程加载的内容公开自定义 API。
3. 启用上下文隔离
此建议是 Electron 自 12.0.0 版以来的默认行为。
上下文隔离是 Electron 的一项功能,允许开发人员在专用 JavaScript 上下文中运行预加载脚本和 Electron API 中的代码。实际上,这意味着像Array.prototype.push
或JSON.parse
这样的全局对象无法被在渲染器进程中运行的脚本修改。
Electron 使用与 Chromium 的内容脚本相同的技术来启用此行为。
即使使用了nodeIntegration: false
,为了真正强制执行强隔离并防止使用 Node 原语,也**必须**使用contextIsolation
。
有关contextIsolation
是什么以及如何启用它的更多信息,请参阅我们专门的上下文隔离文档。
4. 启用进程沙盒
沙盒是 Chromium 的一项功能,它使用操作系统来显着限制渲染器进程可以访问的内容。您应该在所有渲染器中启用沙盒。不建议在未沙盒化的进程(包括主进程)中加载、读取或处理任何不受信任的内容。
有关进程沙盒是什么以及如何启用它的更多信息,请参阅我们专门的进程沙盒文档。
5. 处理来自远程内容的会话权限请求
您可能在使用 Chrome 时看到过权限请求:每当网站尝试使用用户必须手动批准的功能(如通知)时,它们就会弹出。
该 API 基于Chromium 权限 API并实现了相同类型的权限。
为什么?
默认情况下,Electron 会自动批准所有权限请求,除非开发人员已手动配置自定义处理程序。虽然这是一个可靠的默认值,但注重安全的开发人员可能希望假设恰恰相反。
如何?
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 的默认设置。
您可能已经猜到,在渲染器进程上禁用webSecurity
属性(BrowserWindow
、WebContentsView
或<webview>
)会禁用关键的安全功能。
不要在生产应用程序中禁用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\'']
}
})
})
CSP 元标记
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. 不要对 WebView 使用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 选项
在已禁用 Node.js 集成的渲染器进程中创建的 WebView 将无法自行启用集成。但是,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. 禁用或限制导航
如果您的应用不需要导航,或者只需要导航到已知的页面,最好将导航限制在已知的范围内,不允许任何其他类型的导航。
原因?
导航是一个常见的攻击向量。如果攻击者能够说服您的应用从当前页面导航到其他页面,他们可能会迫使您的应用打开互联网上的网站。即使您的 webContents
配置得更安全(例如,禁用 nodeIntegration
或启用 contextIsolation
),让您的应用打开一个随机的网站也会使利用您的应用的工作变得更容易。
一种常见的攻击模式是,攻击者说服您的应用用户以某种方式与应用交互,从而使其导航到攻击者的页面之一。这通常是通过链接、插件或其他用户生成的内容来完成的。
方法?
如果您的应用不需要导航,您可以在 will-navigate
处理程序中调用 event.preventDefault()
。如果您知道您的应用可能会导航到哪些页面,请在事件处理程序中检查 URL,并且只有在 URL 与您期望的 URL 匹配时才允许导航。
我们建议您使用 Node 的 URL 解析器。简单的字符串比较有时会被欺骗 - startsWith('https://example.com')
测试将允许 https://example.com.attacker.com
通过。
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
和用于创建它的选项。我们建议您注册一个处理程序来监视窗口的创建,并拒绝任何意外的窗口创建。
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 以获取有关用法和潜在错误情况的更多详细信息,并参考我们文档中的 如何翻转保险丝?。