安全
🌐 Security
有关如何正确披露 Electron 漏洞的信息,请参见 SECURITY.md。
🌐 For information on how to properly disclose an Electron vulnerability, see SECURITY.md.
关于上游 Chromium 漏洞:Electron 会随着交替的 Chromium 版本保持更新。更多信息,请参阅 Electron 发布时间表 文档。
🌐 For upstream Chromium vulnerabilities: Electron keeps up to date with alternating Chromium releases. For more information, see the Electron Release Timelines document.
前言
🌐 Preface
作为网页开发者,我们通常享受浏览器提供的强大安全保障——我们编写的代码相关的风险相对较小。我们的网站在沙箱中拥有有限的权限,我们相信用户使用的是由大型工程师团队构建的浏览器,该团队能够迅速应对新发现的安全威胁。
🌐 As web developers, we usually enjoy the strong security net of the browser — the risks associated with the code we write are relatively small. Our websites are granted limited powers in a sandbox, and we trust that our users enjoy a browser built by a large team of engineers that is able to quickly respond to newly discovered security threats.
在使用 Electron 时,重要的是要明白 Electron 不是一个网页浏览器。它允许你使用熟悉的网页技术构建功能丰富的桌面应用,但你的代码拥有更强大的权限。JavaScript 可以访问文件系统、用户的 shell 等功能。这使你能够构建高质量的原生应用,但随着代码被赋予的额外权限,固有的安全风险也随之增加。
🌐 When working with Electron, it is important to understand that Electron is not a web browser. It allows you to build feature-rich desktop applications with familiar web technologies, but your code wields much greater power. JavaScript can access the filesystem, user shell, and more. This allows you to build high quality native applications, but the inherent security risks scale with the additional powers granted to your code.
考虑到这一点,请注意,从不受信任的来源显示任意内容存在严重的安全风险,而 Electron 并不打算处理这种风险。事实上,最受欢迎的 Electron 应用(如 Atom、Slack、Visual Studio Code 等)主要显示本地内容(或可信、安全的远程内容且没有 Node 集成)——如果你的应用执行来自在线来源的代码,确保代码不含恶意内容的责任就在于你自己。
🌐 With that in mind, be aware that displaying arbitrary content from untrusted sources poses a severe security risk that Electron is not intended to handle. In fact, the most popular Electron apps (Atom, Slack, Visual Studio Code, etc) display primarily local content (or trusted, secure remote content without Node integration) — if your application executes code from an online source, it is your responsibility to ensure that the code is not malicious.
一般准则
🌐 General guidelines
安全是每个人的责任
🌐 Security is everyone's responsibility
重要的是要记住,你的 Electron 应用的安全性取决于框架基础(Chromium、Node.js)、Electron 本身、所有 NPM 依赖以及你的代码的整体安全性。因此,遵循一些重要的最佳实践是你的责任:
🌐 It is important to remember that the security of your Electron application is the result of the overall security of the framework foundation (Chromium, Node.js), Electron itself, all NPM dependencies and your code. As such, it is your responsibility to follow a few important best practices:
- 保持你的应用与最新的 Electron 框架版本同步。 在发布产品时,你同时会发布一个由 Electron、Chromium 共享库和 Node.js 组成的打包包。影响这些组件的漏洞可能会影响你应用的安全性。通过将 Electron 更新到最新版本,你可以确保关键漏洞(例如 nodeIntegration 绕过)已被修复,并且无法在你的应用中被利用。更多信息,请参见 “使用当前版本的 Electron”。
- 评估你的依赖。 虽然 NPM 提供了五十万个可重用的包,但选择可靠的第三方库是你的责任。如果你使用的旧库存在已知漏洞,或者依赖维护不善的代码,你的应用安全可能会受到威胁。
- 采用安全编码实践。 应用的第一道防线是你自己的代码。常见的网络漏洞,如跨站脚本(XSS),对 Electron 应用的安全影响更大,因此强烈建议采用安全的软件开发最佳实践并进行安全测试。
隔离不受信任的内容
🌐 Isolation for untrusted content
只要你从不受信任的来源(例如远程服务器)接收代码并在本地执行,就存在安全问题。例如,考虑在默认 BrowserWindow 中显示的远程网站。如果攻击者以某种方式修改了该内容(无论是直接攻击源,还是在你的应用和实际目标之间进行中间人攻击),他们都将能够在用户的计算机上执行本地代码。
🌐 A security issue exists whenever you receive code from an untrusted source (e.g.
a remote server) and execute it locally. As an example, consider a remote
website being displayed inside a default BrowserWindow. If
an attacker somehow manages to change said content (either by attacking the
source directly, or by sitting between your app and the actual destination), they
will be able to execute native code on the user's machine.
在任何情况下都不应在启用 Node.js 集成的情况下加载和执行远程代码。相反,应仅使用本地文件(与你的应用一起打包)来执行 Node.js 代码。要显示远程内容,请使用 <webview> 标签或 WebContentsView,并确保禁用 nodeIntegration 并启用 contextIsolation。
🌐 Under no circumstances should you load and execute remote code with
Node.js integration enabled. Instead, use only local files (packaged together
with your application) to execute Node.js code. To display remote content, use
the <webview> tag or a WebContentsView
and make sure to disable the nodeIntegration and enable contextIsolation.
安全警告和建议会打印到开发者控制台中。它们仅在二进制文件名为 Electron 时显示,这表明开发者当前正在查看控制台。
🌐 Security warnings and recommendations are printed to the developer console. They only show up when the binary's name is Electron, indicating that a developer is currently looking at the console.
你可以通过在 process.env 或 window 对象上设置 ELECTRON_ENABLE_SECURITY_WARNINGS 或 ELECTRON_DISABLE_SECURITY_WARNINGS 来强制启用或禁用这些警告。
🌐 You can force-enable or force-disable these warnings by setting
ELECTRON_ENABLE_SECURITY_WARNINGS or ELECTRON_DISABLE_SECURITY_WARNINGS on
either process.env or the window object.
清单:安全建议
🌐 Checklist: Security recommendations
你至少应该遵循以下步骤来提高应用的安全性:
🌐 You should at least follow these steps to improve the security of your application:
- 仅加载安全内容
- 不要为远程内容启用 Node.js 集成
- 在所有渲染器中启用上下文隔离
- 启用进程沙盒
- 在加载远程内容的所有会话中使用
ses.setPermissionRequestHandler() - 不要禁用
webSecurity - 定义一个
Content-Security-Policy并使用限制性规则(即script-src 'self') - 不要启用
allowRunningInsecureContent - 不要启用实验性功能 10. 不要使用
enableBlinkFeatures11.<webview>:不要使用allowpopups12.<webview>:验证选项和参数 13. 禁用或限制导航 14. 禁用或限制新窗口的创建 15. 不要在不受信任的内容中使用shell.openExternal16. 使用最新版本的 Electron 17. 验证所有 IPC 消息的sender18. 避免使用file://协议,优先使用自定义协议 19. 检查可以更改的熔丝 20. 不要向不受信任的网页内容暴露 Electron API
1. 仅加载安全内容
🌐 1. Only load secure content
任何未随应用包含的资源都应使用安全协议加载,例如 HTTPS。换句话说,不要使用像 HTTP 这样的不安全协议。同样,我们建议使用 WSS 代替 WS,使用 FTPS 代替 FTP,等等。
🌐 Any resources not included with your application should be loaded using a
secure protocol like HTTPS. In other words, do not use insecure protocols
like HTTP. Similarly, we recommend the use of WSS over WS, FTPS over
FTP, and so on.
为什么?
🌐 Why?
HTTPS 有两个主要好处:
- 它确保数据完整性,保证数据在你的应用与主机之间传输时未被篡改。
- 它会加密你与目标主机之间的流量,使得窃听你应用与主机之间传输的信息变得更加困难。
如何?
🌐 How?
// 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 集成
🌐 2. Do not enable Node.js integration for remote content
自 5.0.0 以来,此建议是 Electron 中的默认行为。
🌐 This recommendation is the default behavior in Electron since 5.0.0.
绝对重要的是,你不要在加载远程内容的任何渲染器(BrowserWindow、WebContentsView 或 <webview>)中启用 Node.js 集成。目标是限制你授予远程内容的权限,从而大大增加攻击者在能够在你的网站上执行 JavaScript 时伤害用户的难度。
🌐 It is paramount that you do not enable Node.js integration in any renderer
(BrowserWindow, WebContentsView, or
<webview>) that loads remote content. The goal is to limit the
powers you grant to remote content, thus making it dramatically more difficult
for an attacker to harm your users should they gain the ability to execute
JavaScript on your website.
之后,你可以为特定主机授予额外的权限。例如,如果你打开一个指向 https://example.com/ 的浏览器窗口,你可以仅赋予该网站它所需的权限,而不超出这些权限。
🌐 After this, you can grant additional permissions for specific hosts. For example,
if you are opening a BrowserWindow pointed at https://example.com/, you can
give that website exactly the abilities it needs, but no more.
为什么?
🌐 Why?
如果攻击者能够跳出渲染器进程并在用户的计算机上执行代码,跨站脚本(XSS)攻击将更为危险。跨站脚本攻击相当常见——虽然是个问题,但它们的威力通常仅限于干扰它们所执行的网站。禁用 Node.js 集成有助于防止 XSS 攻击升级为所谓的“远程代码执行”(RCE)攻击。
🌐 A cross-site-scripting (XSS) attack is more dangerous if an attacker can jump out of the renderer process and execute code on the user's computer. Cross-site-scripting attacks are fairly common - and while an issue, their power is usually limited to messing with the website that they are executed on. Disabling Node.js integration helps prevent an XSS from being escalated into a so-called "Remote Code Execution" (RCE) attack.
如何?
🌐 How?
// 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。
🌐 When disabling Node.js integration, you can still expose APIs to your website that
do consume Node.js modules or features. Preload scripts continue to have access
to require and other Node.js features, allowing developers to expose a custom
API to remotely loaded content via the contextBridge API.
3. 启用上下文隔离
🌐 3. Enable Context Isolation
自 12.0.0 以来,上下文隔离是 Electron 中的默认行为。
🌐 Context Isolation is the default behavior in Electron since 12.0.0.
上下文隔离是 Electron 的一个功能,它允许开发者在预加载脚本和 Electron API 中在一个独立的 JavaScript 上下文中运行代码。实际上,这意味着像 Array.prototype.push 或 JSON.parse 这样的全局对象不能被在渲染进程中运行的脚本修改。
🌐 Context isolation is an Electron feature that allows developers to run code
in preload scripts and in Electron APIs in a dedicated JavaScript context. In
practice, that means that global objects like Array.prototype.push or
JSON.parse cannot be modified by scripts running in the renderer process.
Electron 使用与 Chromium 的 内容脚本 相同的技术来实现此行为。
🌐 Electron uses the same technology as Chromium's Content Scripts to enable this behavior.
即使使用 nodeIntegration: false,要真正实现强隔离并防止使用 Node 原语,也必须使用 contextIsolation。
🌐 Even when nodeIntegration: false is used, to truly enforce strong isolation
and prevent the use of Node primitives contextIsolation must also be used.
注意,通过设置 nodeIntegration: true 为渲染进程 禁用上下文隔离 同时也会 禁用该进程的进程沙箱。请参见下文中的章节。
🌐 Beware that disabling context isolation for a renderer process by setting
nodeIntegration: true also disables process sandboxing for that process.
See section below.
有关 contextIsolation 是什么以及如何启用它的更多信息,请参阅我们专门的 上下文隔离 文档。
🌐 For more information on what contextIsolation is and how to enable it please
see our dedicated Context Isolation document.
4. 启用进程沙箱
🌐 4. Enable process sandboxing
此建议是 Electron 自 20.0.0 版本以来的默认行为。
🌐 This recommendation is the default behavior in Electron since 20.0.0.
此外,可以对所有渲染器进程在整个应用范围内强制实现进程沙箱:全局启用沙箱
🌐 Additionally, process sandboxing can be enforced for all renderer processes application wide: Enabling the sandbox globally
禁用上下文隔离(见上文)也会禁用进程沙箱,无论默认设置、sandbox: false 还是全局启用的沙箱功能!
🌐 Disabling context isolation (see above) also disables process sandboxing,
regardless of the default, sandbox: false or globally enabled sandboxing!
沙盒 是 Chromium 的一项功能,它利用操作系统显著限制渲染进程的访问权限。你应该在所有渲染器中启用沙盒。不建议在未启用沙盒的进程中加载、读取或处理任何不受信任的内容,包括主进程。
有关进程沙箱的更多信息以及如何启用它,请参阅我们专门的 进程沙箱 文档。
🌐 For more information on what Process Sandboxing is and how to enable it please see our dedicated Process Sandboxing document.
5. 处理来自远程内容的会话权限请求
🌐 5. Handle session permission requests from remote content
在使用 Chrome 时,你可能会看到权限请求:每当网站尝试使用需要用户手动批准的功能(例如通知)时,它们就会弹出。
🌐 You may have seen permission requests while using Chrome: they pop up whenever the website attempts to use a feature that the user has to manually approve ( like notifications).
该 API 基于 Chromium 权限 API,并实现了相同类型的权限。
🌐 The API is based on the Chromium permissions API and implements the same types of permissions.
为什么?
🌐 Why?
默认情况下,Electron 会自动批准所有权限请求,除非开发者手动配置了自定义处理程序。虽然这是一个可靠的默认设置,但注重安全的开发者可能更倾向于采取完全相反的策略。
🌐 By default, Electron will automatically approve all permission requests unless the developer has manually configured a custom handler. While a solid default, security-conscious developers might want to assume the very opposite.
如何?
🌐 How?
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)
}
})
注意:只有在调用 app.whenReady 之后,session.defaultSession 才可用。
🌐 Note: session.defaultSession is only available after app.whenReady is called.
6. 不要禁用 webSecurity
🌐 6. Do not disable webSecurity
此建议是 Electron 的默认设置。
🌐 This recommendation is Electron's default.
你可能已经猜到,在渲染进程(BrowserWindow、WebContentsView 或 <webview>)上禁用 webSecurity 属性会禁用关键的安全功能。
🌐 You may have already guessed that disabling the webSecurity property on a
renderer process (BrowserWindow,
WebContentsView, or <webview>) disables
crucial security features.
不要在生产环境的应用中禁用 webSecurity。
🌐 Do not disable webSecurity in production applications.
为什么?
🌐 Why?
禁用 webSecurity 将会禁用同源策略,并将 allowRunningInsecureContent 属性设置为 true。换句话说,它允许执行来自不同域的不安全代码。
🌐 Disabling webSecurity will disable the same-origin policy and set
allowRunningInsecureContent property to true. In other words, it allows
the execution of insecure code from different domains.
如何?
🌐 How?
// 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. 定义内容安全策略
🌐 7. Define a Content Security Policy
内容安全策略(CSP)是防止跨站脚本攻击和数据注入攻击的额外保护层。我们建议在 Electron 中加载的任何网站都启用它们。
🌐 A Content Security Policy (CSP) is an additional layer of protection against cross-site-scripting attacks and data injection attacks. We recommend that they be enabled by any website you load inside Electron.
为什么?
🌐 Why?
CSP 允许提供内容的服务器限制和控制 Electron 可以为特定网页加载的资源。应允许 https://example.com 从你定义的来源加载脚本,而来自 https://evil.attacker.com 的脚本不应被允许运行。定义 CSP 是提高应用安全性的简单方法。
🌐 CSP allows the server serving content to restrict and control the resources
Electron can load for that given web page. https://example.com should
be allowed to load scripts from the origins you defined while scripts from
https://evil.attacker.com should not be allowed to run. Defining a CSP is an
easy way to improve your application's security.
如何?
🌐 How?
以下 CSP 将允许 Electron 执行来自当前网站和 apis.example.com 的脚本。
🌐 The following CSP will allow Electron to execute scripts from the current
website and from apis.example.com.
// Bad
Content-Security-Policy: '*'
// Good
Content-Security-Policy: script-src 'self' https://apis.example.com
CSP HTTP 标头
🌐 CSP HTTP headers
Electron 会尊重 Content-Security-Policy HTTP 头,可以使用 Electron 的 webRequest.onHeadersReceived 处理程序来设置:
🌐 Electron respects the Content-Security-Policy HTTP header
which can be set using Electron's
webRequest.onHeadersReceived
handler:
const { session } = require('electron')
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src \'none\'']
}
})
})
注意:只有在调用 app.whenReady 之后,session.defaultSession 才可用。
🌐 Note: session.defaultSession is only available after app.whenReady is called.
CSP 元标记
🌐 CSP meta tag
CSP 首选的传递机制是 HTTP 头。然而,在使用 file:// 协议加载资源时无法使用此方法。在某些情况下,直接在页面标记中使用 <meta> 标签设置策略可能会很有用:
🌐 CSP's preferred delivery mechanism is an HTTP header. However, it is not possible
to use this method when loading a resource using the file:// protocol. It can
be useful in some cases to set a policy on a page directly in the markup using a
<meta> tag:
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
8. 不要启用 allowRunningInsecureContent
🌐 8. Do not enable allowRunningInsecureContent
此建议是 Electron 的默认设置。
🌐 This recommendation is Electron's default.
默认情况下,Electron 不允许通过 HTTPS 加载的网站从不安全的来源 (HTTP) 加载和执行脚本、CSS 或插件。将属性 allowRunningInsecureContent 设置为 true 可以禁用该保护。
🌐 By default, Electron will not allow websites loaded over HTTPS to load and
execute scripts, CSS, or plugins from insecure sources (HTTP). Setting the
property allowRunningInsecureContent to true disables that protection.
通过 HTTPS 加载网站的初始 HTML 并尝试通过 HTTP 加载后续资源也被称为“混合内容”。
🌐 Loading the initial HTML of a website over HTTPS and attempting to load
subsequent resources via HTTP is also known as "mixed content".
为什么?
🌐 Why?
通过 HTTPS 加载内容可以确保所加载资源的真实性和完整性,同时对传输流量本身进行加密。有关更多详情,请参阅仅显示安全内容一节。
🌐 Loading content over HTTPS assures the authenticity and integrity
of the loaded resources while encrypting the traffic itself. See the section on
only displaying secure content for more details.
如何?
🌐 How?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
// Good
const mainWindow = new BrowserWindow({})
9. 不要启用实验性功能
🌐 9. Do not enable experimental features
此建议是 Electron 的默认设置。
🌐 This recommendation is Electron's default.
Electron 的高级用户可以使用 experimentalFeatures 属性启用实验性的 Chromium 功能。
🌐 Advanced users of Electron can enable experimental Chromium features using the
experimentalFeatures property.
为什么?
🌐 Why?
正如其名称所示,实验性功能是实验性的,并未向所有 Chromium 用户开放。此外,它们对整个 Electron 的影响很可能尚未经过测试。
🌐 Experimental features are, as the name suggests, experimental and have not been enabled for all Chromium users. Furthermore, their impact on Electron as a whole has likely not been tested.
存在合法的使用场景,但除非你知道自己在做什么,否则不应启用此属性。
🌐 Legitimate use cases exist, but unless you know what you are doing, you should not enable this property.
如何?
🌐 How?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
// Good
const mainWindow = new BrowserWindow({})
10. 不要使用 enableBlinkFeatures
🌐 10. Do not use enableBlinkFeatures
此建议是 Electron 的默认设置。
🌐 This recommendation is Electron's default.
Blink 是 Chromium 背后的渲染引擎的名称。与 experimentalFeatures 一样,enableBlinkFeatures 属性允许开发者启用默认情况下被禁用的功能。
🌐 Blink is the name of the rendering engine behind Chromium. As with
experimentalFeatures, the enableBlinkFeatures property allows developers to
enable features that have been disabled by default.
为什么?
🌐 Why?
一般来说,如果某个功能默认未启用,通常是有充分的理由的。启用特定功能是有合理使用场景的。作为开发者,你应该清楚自己为什么需要启用某个功能,其可能产生的影响是什么,以及它如何影响应用的安全性。在任何情况下,都不应出于猜测而启用功能。
🌐 Generally speaking, there are likely good reasons if a feature was not enabled by default. Legitimate use cases for enabling specific features exist. As a developer, you should know exactly why you need to enable a feature, what the ramifications are, and how it impacts the security of your application. Under no circumstances should you enable features speculatively.
如何?
🌐 How?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
// Good
const mainWindow = new BrowserWindow()
11. 不要在 WebViews 中使用 allowpopups
🌐 11. Do not use allowpopups for WebViews
此建议是 Electron 的默认设置。
🌐 This recommendation is Electron's default.
如果你正在使用 <webview>,你可能需要在你的 <webview> 标签中加载页面和脚本,以便打开新窗口。allowpopups 属性使它们能够使用 window.open() 方法创建新的 BrowserWindows。否则,<webview> 标签不允许创建新窗口。
🌐 If you are using <webview>, you might need the pages and scripts
loaded in your <webview> tag to open new windows. The allowpopups attribute
enables them to create new BrowserWindows using the
window.open() method. <webview> tags are otherwise not allowed to create new
windows.
为什么?
🌐 Why?
如果你不需要弹出窗口,最好默认不允许创建新的 BrowserWindows。这遵循最小必要访问的原则:除非你确定网站需要该功能,否则不要让它创建新的弹出窗口。
🌐 If you do not need popups, you are better off not allowing the creation of
new BrowserWindows by default. This follows the principle
of minimally required access: Don't let a website create new popups unless
you know it needs that feature.
如何?
🌐 How?
<!-- Bad -->
<webview allowpopups src="page.html"></webview>
<!-- Good -->
<webview src="page.html"></webview>
12. 在创建之前验证 WebView 选项
🌐 12. Verify WebView options before creation
在没有启用 Node.js 集成的渲染器进程中创建的 WebView 无法自行启用集成。然而,WebView 始终会创建一个具有自己 webPreferences 的独立渲染器进程。
🌐 A WebView created in a renderer process that does not have Node.js integration
enabled will not be able to enable integration itself. However, a WebView will
always create an independent renderer process with its own webPreferences.
从主进程控制新 <webview> 标签的创建,并验证它们的 webPreferences 没有禁用安全功能,这是一个好主意。
🌐 It is a good idea to control the creation of new <webview> tags
from the main process and to verify that their webPreferences do not disable
security features.
为什么?
🌐 Why?
由于 <webview> 存在于 DOM 中,即使 Node.js 集成被禁用,也可以通过在你的网站上运行的脚本来创建它们。
🌐 Since <webview> live in the DOM, they can be created by a script running on your
website even if Node.js integration is otherwise disabled.
Electron 允许开发者禁用控制渲染器进程的各种安全功能。在大多数情况下,开发者不需要禁用这些功能,因此你不应该允许为新创建的 <webview> 标签使用不同的配置。
🌐 Electron enables developers to disable various security features that control
a renderer process. In most cases, developers do not need to disable any of
those features - and you should therefore not allow different configurations
for newly created <webview> tags.
如何?
🌐 How?
在附加 <webview> 标签之前,Electron 会在托管的 webContents 上触发 will-attach-webview 事件。使用该事件可以防止以可能不安全的选项创建 webViews。
🌐 Before a <webview> tag is attached, Electron will fire the
will-attach-webview event on the hosting webContents. Use the event to
prevent the creation of webViews with possibly insecure options.
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()
}
})
})
再次说明,这个列表只能降低风险,但无法消除风险。如果你的目标是展示一个网站,使用浏览器会是更安全的选择。
🌐 Again, this list merely minimizes the risk, but does not remove it. If your goal is to display a website, a browser will be a more secure option.
13. 禁用或限制导航
🌐 13. Disable or limit navigation
如果你的应用不需要导航,或者只需要导航到已知页面,那么将导航限制在已知范围内,不允许任何其他类型的导航,是一个好主意。
🌐 If your app has no need to navigate or only needs to navigate to known pages, it is a good idea to limit navigation outright to that known scope, disallowing any other kinds of navigation.
为什么?
🌐 Why?
导航是一个常见的攻击向量。如果攻击者能够说服你的应用导航离开当前页面,他们可能会强制你的应用打开互联网上的网站。即使你的 webContents 配置得更安全(例如禁用 nodeIntegration 或启用 contextIsolation),让你的应用打开一个随机网站仍会让利用你应用的工作变得容易得多。
🌐 Navigation is a common attack vector. If an attacker can convince your app to
navigate away from its current page, they can possibly force your app to open
web sites on the Internet. Even if your webContents are configured to be more
secure (like having nodeIntegration disabled or contextIsolation enabled),
getting your app to open a random web site will make the work of exploiting your
app a lot easier.
一种常见的攻击模式是攻击者说服你的应用用户以某种方式与应用互动,从而使应用导航到攻击者的页面。这通常通过链接、插件或其他用户生成的内容来实现。
🌐 A common attack pattern is that the attacker convinces your app's users to interact with the app in such a way that it navigates to one of the attacker's pages. This is usually done via links, plugins, or other user-generated content.
如何?
🌐 How?
如果你的应用不需要导航,你可以在 will-navigate 处理程序中调用 event.preventDefault()。如果你知道你的应用可能会导航到哪些页面,可以在事件处理程序中检查 URL,并且只有当它与你预期的 URL 匹配时才允许导航发生。
🌐 If your app has no need for navigation, you can call event.preventDefault()
in a will-navigate handler. If you know which pages your app
might navigate to, check the URL in the event handler and only let navigation
occur if it matches the URLs you're expecting.
我们建议你使用 Node 的 URL 解析器。简单的字符串比较有时会被蒙混过去——一个 startsWith('https://example.com') 测试可能会让 https://example.com.attacker.com 通过。
🌐 We recommend that you use Node's parser for URLs. Simple string comparisons can
sometimes be fooled - a startsWith('https://example.com') test would let
https://example.com.attacker.com through.
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. 禁用或限制新窗口的创建
🌐 14. Disable or limit creation of new windows
如果你有一组已知的窗口,限制在应用中创建额外窗口是一个好主意。
🌐 If you have a known set of windows, it's a good idea to limit the creation of additional windows in your app.
为什么?
🌐 Why?
就像导航一样,创建新的 webContents 是一种常见的攻击方式。攻击者试图让你的应用创建具有比之前更多权限的新窗口、框架或其他渲染进程;或者打开之前无法打开的页面。
🌐 Much like navigation, the creation of new webContents is a common attack
vector. Attackers attempt to convince your app to create new windows, frames,
or other renderer processes with more privileges than they had before; or
with pages opened that they couldn't open before.
如果除了你知道需要创建的窗口之外不需要创建其他窗口,禁用窗口创建可以以零成本为你提供额外的安全性。这通常适用于只打开一个 BrowserWindow 并且在运行时不需要打开任意数量额外窗口的应用。
🌐 If you have no need to create windows in addition to the ones you know you'll
need to create, disabling the creation buys you a little bit of extra
security at no cost. This is commonly the case for apps that open one
BrowserWindow and do not need to open an arbitrary number of additional
windows at runtime.
如何?
🌐 How?
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
🌐 15. Do not use shell.openExternal with untrusted content
shell 模块的 openExternal API 允许使用桌面本地工具打开给定的协议 URI。例如,在 macOS 上,此功能类似于 open 终端命令工具,并将根据 URI 和文件类型关联打开特定应用。
🌐 The shell module's openExternal API allows opening a given
protocol URI with the desktop's native utilities. On macOS, for instance, this
function is similar to the open terminal command utility and will open the
specific application based on the URI and filetype association.
为什么?
🌐 Why?
openExternal的不当使用可能被用来破坏用户的主机。当openExternal与不受信任的内容一起使用时,可能被用来执行任意命令。
🌐 Improper use of openExternal can be leveraged to compromise
the user's host. When openExternal is used with untrusted content, it can be
leveraged to execute arbitrary commands.
如何?
🌐 How?
// 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
🌐 16. Use a current version of Electron
你应该努力始终使用最新可用版本的 Electron。每当发布新的主要版本时,你应尽快尝试更新你的应用。
🌐 You should strive for always using the latest available version of Electron. Whenever a new major version is released, you should attempt to update your app as quickly as possible.
为什么?
🌐 Why?
使用较旧版本的 Electron、Chromium 和 Node.js 构建的应用相比使用这些组件更新版本的应用更容易成为攻击目标。一般来说,针对旧版本 Chromium 和 Node.js 的安全问题和漏洞更为普遍。
🌐 An application built with an older version of Electron, Chromium, and Node.js is an easier target than an application that is using more recent versions of those components. Generally speaking, security issues and exploits for older versions of Chromium and Node.js are more widely available.
Chromium 和 Node.js 都是由成千上万名才华横溢的开发者打造的令人印象深刻的工程成就。鉴于它们的受欢迎程度,其安全性会由同样技术高超的安全研究人员进行仔细测试和分析。许多这些研究人员 负责任地披露漏洞,这通常意味着研究人员会给 Chromium 和 Node.js 一些时间来修复问题,然后再发布它们。如果你的应用运行的是最新版本的 Electron(因此也包括 Chromium 和 Node.js),潜在的安全问题就不那么广为人知,应用会更安全。
🌐 Both Chromium and Node.js are impressive feats of engineering built by thousands of talented developers. Given their popularity, their security is carefully tested and analyzed by equally skilled security researchers. Many of those researchers disclose vulnerabilities responsibly, which generally means that researchers will give Chromium and Node.js some time to fix issues before publishing them. Your application will be more secure if it is running a recent version of Electron (and thus, Chromium and Node.js) for which potential security issues are not as widely known.
如何?
🌐 How?
每次迁移你的应用一个主要版本,同时参考 Electron 的 重大变化 文档,查看是否有代码需要更新。
🌐 Migrate your app one major version at a time, while referring to Electron's Breaking Changes document to see if any code needs to be updated.
17. 验证所有 IPC 消息的 sender
🌐 17. Validate the sender of all IPC messages
你应该始终验证传入 IPC 消息的 sender 属性,以确保不会对不受信任的渲染器执行操作或发送信息。
🌐 You should always validate incoming IPC messages sender property to ensure you
aren't performing actions or sending information to untrusted renderers.
为什么?
🌐 Why?
理论上,所有网页框架都可以向主进程发送 IPC 消息,包括某些情况下的 iframe 和子窗口。如果你有一个 IPC 消息通过 event.reply 返回用户数据给发送方,或执行渲染器本身无法执行的特权操作,你应该确保不会监听第三方网页框架。
🌐 All Web Frames can in theory send IPC messages to the main process, including
iframes and child windows in some scenarios. If you have an IPC message that returns
user data to the sender via event.reply or performs privileged actions that the renderer
can't natively, you should ensure you aren't listening to third party web frames.
默认情况下,你应该验证 所有 IPC 消息的 sender。
🌐 You should be validating the sender of all IPC messages by default.
如何?
🌐 How?
// Bad
ipcMain.handle('get-secrets', () => {
return getSecrets()
})
// Good
ipcMain.handle('get-secrets', (e) => {
if (!validateSender(e.senderFrame)) return null
return getSecrets()
})
function validateSender (frame) {
// Validate 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:// 协议,建议使用自定义协议
🌐 18. Avoid usage of the file:// protocol and prefer usage of custom protocols
你应该通过自定义协议而不是 file:// 协议来提供本地页面。
🌐 You should serve local pages from a custom protocol instead of the file:// protocol.
为什么?
🌐 Why?
file:// 协议在 Electron 中比在网页浏览器中拥有更多权限,即使在浏览器中,它对待该协议也与对待 http/https URL 的方式不同。使用自定义协议可以让你更贴近传统网页 URL 的行为,同时保留更多对加载内容及加载时机的控制权。
🌐 The file:// protocol gets more privileges in Electron than in a web browser and even in
browsers it is treated differently to http/https URLs. Using a custom protocol allows you
to be more aligned with classic web url behavior while retaining even more control about
what can be loaded and when.
在 file:// 上运行的页面可以单方面访问你计算机上的每个文件,这意味着 XSS 问题可以被用来从用户的计算机加载任意文件。使用自定义协议可以防止此类问题,因为你可以将协议限制为仅提供特定文件集。
🌐 Pages running on file:// have unilateral access to every file on your machine meaning
that XSS issues can be used to load arbitrary files from the users machine. Using a custom
protocol prevents issues like this as you can limit the protocol to only serving a specific
set of files.
如何?
🌐 How?
参考 protocol.handle 示例,学习如何通过自定义协议提供文件/内容。
🌐 Follow the protocol.handle examples to
learn how to serve files / content from a custom protocol.
19. 检查哪些保险丝可以更换
🌐 19. Check which fuses you can change
Electron 提供了许多可能有用的选项,但大部分应用可能并不需要。为了避免必须自己构建 Electron 版本,这些选项可以通过 Fuses 来关闭或开启。
🌐 Electron ships with a number of options that can be useful but a large portion of applications probably don't need. In order to avoid having to build your own version of Electron, these can be turned off or on using Fuses.
为什么?
🌐 Why?
一些保险丝,如 runAsNode 和 nodeCliInspect,允许应用在使用特定环境变量或命令行参数从命令行运行时表现不同。这些可以用来通过你的应用在设备上执行命令。
🌐 Some fuses, like runAsNode and nodeCliInspect, allow the application to behave differently
when run from the command line using specific environment variables or CLI arguments. These
can be used to execute commands on the device through your application.
这可以让外部脚本运行它们可能本不被允许的命令,但你的应用可能拥有这些权限。
🌐 This can let external scripts run commands that they potentially would not be allowed to, but that your application might have the rights for.
如何?
🌐 How?
@electron/fuses 是我们制作的一个模块,用于轻松切换这些保险丝。有关使用方法和潜在错误案例的更多详细信息,请查看该模块的 README,并参阅我们文档中的 我如何切换保险丝? 。
20. 不要将 Electron API 暴露给不受信任的网页内容
🌐 20. Do not expose Electron APIs to untrusted web content
你不应该在预加载脚本中直接将 Electron 的 API,尤其是 IPC,暴露给不受信任的网页内容。
🌐 You should not directly expose Electron's APIs, especially IPC, to untrusted web content in your preload scripts.
为什么?
🌐 Why?
暴露像 ipcRenderer.on 这样的原始 API 是危险的,因为它会让渲染进程直接访问整个 IPC 事件系统,从而允许它们监听任何 IPC 事件,而不仅仅是那些为它们预期的事件。
🌐 Exposing raw APIs like ipcRenderer.on is dangerous because it gives renderer processes direct
access to the entire IPC event system, allowing them to listen for any IPC events, not just the ones
intended for them.
为了避免这种暴露,我们也不能直接传递回调:IPC 事件回调的第一个参数是一个 IpcRendererEvent 对象,其中包括像 sender 这样的属性,这些属性可以访问底层的 ipcRenderer 实例。即使你只监听特定事件,直接传递回调也意味着渲染进程可以访问这个事件对象。
🌐 To avoid that exposure, we also cannot pass callbacks directly through: The first
argument to IPC event callbacks is an IpcRendererEvent object, which includes properties like sender
that provide access to the underlying ipcRenderer instance. Even if you only listen for specific
events, passing the callback directly means the renderer gets access to this event object.
简而言之,我们希望不受信任的 Web 内容只能访问必要的信息和 API。
🌐 In short, we want the untrusted web content to only have access to necessary information and APIs.
如何?
🌐 How?
// 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 的更多信息以及如何使用它来保护你的应用,请参阅 Context Isolation 文档。
🌐 For more information on what contextIsolation is and how to use it to secure your app,
please see the Context Isolation document.