安全
有关如何正确披露 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://
协议,并优先使用自定义协议 - 检查您可以更改哪些保险丝
- 不要向不受信任的 Web 内容暴露 Electron API
要自动化检测错误配置和不安全模式,可以使用 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 集成时,您仍然可以向您的网站暴露使用 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 会自动批准所有权限请求,除非开发人员手动配置了自定义处理程序。 虽然这是一个稳妥的默认设置,但注重安全的开发人员可能希望采取完全相反的假设。
如何做?
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 的默认设置。
您可能已经猜到,在渲染器进程(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\'']
}
})
})
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 之前验证其选项
在未启用 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 匹配时才允许导航发生。
我们建议您使用 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 和子窗口。 如果您有一个通过 event.reply
将用户数据返回给发送者或执行渲染器无法原生执行的特权操作的 IPC 消息,您应确保您没有监听来自第三方的 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 版本,可以使用保险丝 (Fuses) 来打开或关闭这些选项。
为什么?
一些保险丝,如 runAsNode
和 nodeCliInspect
,允许应用程序在使用特定环境变量或 CLI 参数从命令行运行时表现不同。 这些可以用来通过您的应用程序在设备上执行命令。
这可能让外部脚本运行它们可能不被允许的命令,但您的应用程序可能拥有这些权限。
如何做?
我们制作了一个模块,@electron/fuses
,来使翻转这些保险丝变得容易。 请查看该模块的 README 以获取有关用法和潜在错误情况的更多详细信息,并参考我们文档中的如何翻转保险丝?。
20. 不要向不受信任的 Web 内容暴露 Electron API
您不应在预加载脚本中直接向不受信任的 Web 内容暴露 Electron 的 API,尤其是 IPC。
为什么?
暴露像 ipcRenderer.on
这样的原始 API 是危险的,因为它给了渲染器进程直接访问整个 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
是什么以及如何使用它来保护您的应用的更多信息,请参阅上下文隔离文档。