安全性,原生能力和你的责任
Web开发人员通常享有浏览器强大的网络安全特性,而自己的代码风险相对较小。 我们的网站从沙箱获得有限权限。我们坚信用户可以享受一个大的工程师团队构建的浏览器,因为他们能够快速响应新发现的安全威胁。
当使用 Electron 时,很重要的一点是要理解 Electron 不是一个 Web 浏览器。 它允许您使用熟悉的 Web 技术构建功能丰富的桌面应用程序,但是您的代码具有更强大的功能。 JavaScript 可以访问文件系统,用户 shell 等。 这允许您构建更高质量的本机应用程序,但是内在的安全风险会随着授予您的代码的额外权力而增加。
考虑到这一点,请注意,展示任意来自不受信任源的内容都将会带来严重的安全风险,而这种风险Electron也没打算处理。 事实上,最流行的 Electron 应用程序(Atom,Slack,Visual Studio Code 等) 主要显示本地内容(即使有远程内容也是无 Node 的、受信任的、安全的内容) - 如果您的应用程序要运行在线的源代码,那么您需要确保源代码不是恶意的。
Security, Native Capabilities, and Your Responsibility
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.
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.
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.
报告安全问题
有关如何正确上报 Electron 漏洞的信息,参阅 SECURITY.md
Reporting Security Issues
For information on how to properly disclose an Electron vulnerability, see SECURITY.md
Chromium 安全问题和升级
尽管 Electron 努力尽快支持新版本的 Chromium,但开发人员应该意识到,升级是一项严肃的工作 - 涉及手动编辑几十个甚至几百个文件。 受限于当前可用的资源与贡献,Electron 通常不能保持使用最新版本的 Chromium,可能落后几周或几个月。
我们认为,我们当前的更新 Chromium 组件的系统在我们可用的资源和构建在框架之上的大多数应用程序的需求之间取得了适当的平衡。 我们绝对有兴趣听听更多关于在 Electron 上构建事物的人的具体用例。 非常欢迎提出请求并且捐助支持我们的努力。
Chromium Security Issues and Upgrades
Electron keeps up to date with alternating Chromium releases. For more information, see the Electron Release Cadence blog post.
安全是所有人的共同责任
需要牢记的是,你的 Electron 程序安全性除了依赖于整个框架基础(Chromium、Node.js)、Electron 本身和所有相关 NPM 库的安全性,还依赖于你自己的代码安全性。 因此,你有责任遵循下列安全守则:
使用最新版的 Electron 框架搭建你的程序。你最终发行的产品中会包含 Electron、Chromium 共享库和 Node.js 的组件。 这些组件存在的安全问题也可能影响你的程序安全性。 By updating Electron to the latest version, you ensure that critical vulnerabilities (such as nodeIntegration bypasses) are already patched and cannot be exploited in your application.
Evaluate your dependencies. While NPM provides half a million reusable packages, it is your responsibility to choose trusted 3rd-party libraries. If you use outdated libraries affected by known vulnerabilities or rely on poorly maintained code, your application security could be in jeopardy.
Adopt secure coding practices. The first line of defense for your application is your own code. Common web vulnerabilities, such as Cross-Site Scripting (XSS), have a higher security impact on Electron applications hence it is highly recommended to adopt secure software development best practices and perform security testing.
Security Is Everyone's Responsibility
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:
Keep your application up-to-date with the latest Electron framework release. When releasing your product, you’re also shipping a bundle composed of Electron, Chromium shared library and Node.js. Vulnerabilities affecting these components may impact the security of your application. By updating Electron to the latest version, you ensure that critical vulnerabilities (such as nodeIntegration bypasses) are already patched and cannot be exploited in your application. For more information, see "Use a current version of Electron".
Evaluate your dependencies. While NPM provides half a million reusable packages, it is your responsibility to choose trusted 3rd-party libraries. If you use outdated libraries affected by known vulnerabilities or rely on poorly maintained code, your application security could be in jeopardy.
Adopt secure coding practices. The first line of defense for your application is your own code. Common web vulnerabilities, such as Cross-Site Scripting (XSS), have a higher security impact on Electron applications hence it is highly recommended to adopt secure software development best practices and perform security testing.
Isolation For Untrusted Content
A security issue exists whenever you receive code from an untrusted source (e.g. a remote server) and execute it locally. 例如在默认的 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代码 To display remote content, use the
<webview>
tag orBrowserView
, make sure to disable thenodeIntegration
and enablecontextIsolation
.
Isolation For Untrusted Content
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.
⚠️ 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 orBrowserView
, make sure to disable thenodeIntegration
and enablecontextIsolation
.
Electron 安全警告
从Electron 2.0版本开始,开发者将会在开发者控制台看到打印的警告和建议。 这些警告仅在可执行文件名为 Electron 时才会为开发者显示。
你可以通过在process.env
或 window
对象上配置ELECTRON_ENABLE_SECURITY_WARNINGS
或ELECTRON_DISABLE_SECURITY_WARNINGS
来强制开启或关闭这些警告。
Electron Security Warnings
From Electron 2.0 on, developers will see warnings and recommendations 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.
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.
清单:安全建议
为加强程序安全性,你至少应当遵循下列规则:
- 只加载安全的内容
- 禁止在所有渲染器中使用Node.js集成显示远程内容
- 做所有显示远程内容的渲染器中启用上下文隔离。
- 在所有加载远程内容的会话中使用
ses.setPermissionRequestHandler()
. - 不要禁用
webSecurity
- 定义一个
Content-Security-Policy
并设置限制规则(如:script-src 'self'
) - 不要设置
allowRunningInsecureContent
为 true. - 不要开启实验性功能
- 不要使用
enableBlinkFeatures
<webview>
:不要使用allowpopups
<webview>
:验证选项与参数- 禁用或限制网页跳转
- 禁用或限制新窗口创建
- 不要对不可信的内容使用
openExternal
- 禁用
remote
模块 - Filter the
remote
module
To automate the detection of misconfigurations and insecure patterns, it is possible to use electronegativity. For additional details on potential weaknesses and implementation bugs when developing applications using Electron, please refer to this guide for developers and auditors
Checklist: Security Recommendations
You should at least follow these steps to improve the security of your application:
- Only load secure content
- Disable the Node.js integration in all renderers that display remote content
- Enable context isolation in all renderers that display remote content
- Use
ses.setPermissionRequestHandler()
in all sessions that load remote content - Do not disable
webSecurity
- Define a
Content-Security-Policy
and use restrictive rules (i.e.script-src 'self'
) - Do not set
allowRunningInsecureContent
totrue
- Do not enable experimental features
- Do not use
enableBlinkFeatures
<webview>
: Do not useallowpopups
<webview>
: Verify options and params- Disable or limit navigation
- Disable or limit creation of new windows
- Do not use
openExternal
with untrusted content - Disable the
remote
module - Filter the
remote
module - Use a current version of Electron
To automate the detection of misconfigurations and insecure patterns, it is possible to use electronegativity. For additional details on potential weaknesses and implementation bugs when developing applications using Electron, please refer to this guide for developers and auditors
1) 仅加载安全内容
任何不属于你的应用的资源都应该使用像HTTPS
这样的安全协议来加载。 换言之, 不要使用不安全的协议 (如 HTTP
)。 同理,我们建议使用WSS
,避免使用WS
,建议使用FTPS
,避免使用FTP
,等等诸如此类的协议。
1) Only Load Secure Content
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.
为什么?
HTTPS
有三个主要好处:
1) 它对远程服务器进行身份验证, 确保您的应用程序连接到正确的主机而不是模仿器。 2) 确保数据完整性, 断言数据在应用程序和主机之间传输时未被修改。 3) 它对用户和目标主机之间的通信进行加密, 从而更难窃听应用程序和主机之间发送的信息。
Why?
HTTPS
has three main benefits:
1) It authenticates the remote server, ensuring your app connects to the correct host instead of an impersonator. 2) It ensures data integrity, asserting that the data was not modified while in transit between your application and the host. 3) It encrypts the traffic between your user and the destination host, making it more difficult to eavesdrop on the information sent between your app and the host.
怎么做?
// 不推荐
browserWindow.loadURL ('http://example.com')
// 推荐
browserWindow.loadURL ('https://example.com')
<!-- 不推荐 -->
<script crossorigin src="http://example.com/react.js"></script>
<link rel="stylesheet" href="http://example.com/style.css">
<!-- 推荐 -->
<script crossorigin src="https://example.com/react.js"></script>
<link rel="stylesheet" href="https://example.com/style.css">
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) Do not enable Node.js Integration for Remote Content
This recommendation is the default behavior in Electron since 5.0.0.
It is paramount that you do not enable Node.js integration in any renderer (BrowserWindow
, BrowserView
, or <webview>
) that loads remote content. 其目的是限制您授予远程内容的权限, 从而使攻击者在您的网站上执行 JavaScript 时更难伤害您的用户。
在此之后,你可以为指定的主机授予附加权限。 举例来说,如果你正在打开一个指向 "https://example.com/" 的 BrowserWindow,你可以给它正好所需的权限,无需再多。
2) Do not enable Node.js Integration for Remote Content
This recommendation is the default behavior in Electron since 5.0.0.
It is paramount that you do not enable Node.js integration in any renderer (BrowserWindow
, BrowserView
, 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.
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.
为什么?
如果攻击者跳过渲染进程并在用户电脑上执行恶意代码,那么这种跨站脚本(XSS) 攻击的危害是非常大的。 跨站脚本攻击很常见,通常情况下,威力仅限于执行代码的网站。 禁用Node.js集成有助于防止XSS攻击升级为“远程代码执行” (RCE) 攻击。
Why?
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.
怎么做?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
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')
<!-- 不推荐 -->
<webview nodeIntegration src="page.html"></webview>
<!-- 推荐 -->
<webview src="page.html"></webview>
当禁用Node.js集成时,你依然可以暴露API给你的站点以使用Node.js的模块功能或特性。 预加载脚本依然可以使用require
等Node.js特性, 以使开发者可以暴露自定义API给远程加载内容。
在下面的预加载脚本例子中,后加载的网站内容可以使用window.readConfig()
方法,但不能使用Node.js特性。
const { readFileSync } = require('fs')
window.readConfig = function () {
const data = readFileSync('./config.json')
return data
}
How?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
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>
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.
In the following example preload script, the later loaded website will have access to a window.readConfig()
method, but no Node.js features.
const { readFileSync } = require('fs')
window.readConfig = function () {
const data = readFileSync('./config.json')
return data
}
3) 为远程内容开启上下文隔离
上下文隔离是Electron的一个特性,它允许开发者在预加载脚本里运行代码,里面包含Electron API和专用的JavaScript上下文。 实际上,这意味全局对象如 Array.prototype.push
或 JSON.parse
等无法被渲染进程里的运行脚本修改。
Electron使用了和Chromium相同的Content Scripts技术来开启这个行为。
Even when you use nodeIntegration: false
to enforce strong isolation and prevent the use of Node primitives, contextIsolation
must also be used.
3) Enable Context Isolation for Remote Content
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 uses the same technology as Chromium's Content Scripts to enable this behavior.
Even when you use nodeIntegration: false
to enforce strong isolation and prevent the use of Node primitives, contextIsolation
must also be used.
为什么?
上下文隔离使得每个运行在渲染器上的脚本无需担心改变JavaScript环境变量而与ElectronAPI或预加载脚本发生冲突。
While still an experimental Electron feature, context isolation adds an additional layer of security. It creates a new JavaScript world for Electron APIs and preload scripts, which mitigates so-called "Prototype Pollution" attacks.
同时,预加载脚本依然能访问document
和window
对象。换个角度,就像你以很小的投入却得到双倍回报一样。
Why?
Context isolation allows each the scripts on running in the renderer to make changes to its JavaScript environment without worrying about conflicting with the scripts in the Electron API or the preload script.
While still an experimental Electron feature, context isolation adds an additional layer of security. It creates a new JavaScript world for Electron APIs and preload scripts, which mitigates so-called "Prototype Pollution" attacks.
At the same time, preload scripts still have access to the document
and window
objects. In other words, you're getting a decent return on a likely very small investment.
怎么做?
// Main process
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true,
preload: path.join(app.getAppPath(), 'preload.js')
}
})
// 预加载脚本
// 在页面加载前设置变量
webFrame.executeJavaScript('window.foo = "foo";')
// 这个变量仅限于当前上下文,被加载的页面将无权访问
window.bar = 'bar'
document.addEventListener('DOMContentLoaded', () => {
// 结果为 'undefined',因为 window.foo 仅在主上下文中可用
console.log(window.foo)
// 结果为 'bar',因为 window.bar 定义在本上下文中
console.log(window.bar)
})
How?
// Main process
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true,
preload: path.join(app.getAppPath(), 'preload.js')
}
})
// Preload script
// Set a variable in the page before it loads
webFrame.executeJavaScript('window.foo = "foo";')
// The loaded page will not be able to access this, it is only available
// in this context
window.bar = 'bar'
document.addEventListener('DOMContentLoaded', () => {
// Will log out 'undefined' since window.foo is only available in the main
// context
console.log(window.foo)
// Will log out 'bar' since window.bar is available in this context
console.log(window.bar)
})
4) 处理来自远程内容的会话许可请求
当你使用Chromes时,也许见过这种许可请求:每当网站尝试使用某个特性时,就会弹出让用户手动确认(如网站通知)
此API基于Chromium permissions API,并已实现对应的许可类型。
4) Handle Session Permission Requests From Remote Content
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).
The API is based on the Chromium permissions API and implements the same types of permissions.
为什么?
默认情况下,Electron将自动批准所有的许可请求,除非开发者手动配置一个自定义处理函数。 尽管默认如此,有安全意识的开发者可能希望默认反着来。
Why?
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.
怎么做?
const { session } = require('electron')
session
.fromPartition('some-partition')
.setPermissionRequestHandler((webContents, permission, callback) => {
const url = webContents.getURL()
if (permission === 'notifications') {// Approves the permissions requestcallback(true)
}
// Verify URL
if (!url.startsWith('https://example.com/')) {// Denies the permissions requestreturn callback(false)
}
})
How?
const { session } = require('electron')
session
.fromPartition('some-partition')
.setPermissionRequestHandler((webContents, permission, callback) => {
const url = webContents.getURL()
if (permission === 'notifications') {// Approves the permissions requestcallback(true)
}
// Verify URL
if (!url.startsWith('https://example.com/')) {// Denies the permissions requestreturn callback(false)
}
})
5) 不要禁用WebSecurity
Electron的默认值即是建议值。
在渲染进程(BrowserWindow
、BrowserView
和 <webview>
)中禁用 webSecurity
将导致至关重要的安全性功能被关闭。
不要在生产环境中禁用webSecurity
。
5) Do Not Disable WebSecurity
Recommendation is Electron's default
You may have already guessed that disabling the webSecurity
property on a renderer process (BrowserWindow
, BrowserView
, or <webview>
) disables crucial security features.
Do not disable webSecurity
in production applications.
为什么?
禁用 webSecurity
将会禁止同源策略并且将 allowRunningInsecureContent
属性置 true
。 换句话说,这将使得来自其他站点的非安全代码被执行。
Why?
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.
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false
}
})
// 推荐
const mainWindow = new BrowserWindow()
<!-- 不推荐 -->
<webview disablewebsecurity src="page.html"></webview>
<!-- 推荐 -->
<webview src="page.html"></webview>
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>
6) 定义一个内容安全策略
内容安全策略(CSP) 是应对跨站脚本攻击和数据注入攻击的又一层保护措施。 我们建议任何载入到Electron的站点都要开启。
6) Define a Content Security Policy
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.
为什么?
CSP允许Electron通过服务端内容对指定页面的资源加载进行约束与控制。 如果你定义https://example.com
这个源,所属这个源的脚本都允许被加载,反之https://evil.attacker.com
不会被允许加载运行。 对于提升你的应用安全性,设置CSP是个很方便的办法。
下面的CSP设置使得Electron只能执行自身站点和来自apis.example.com
的脚本。
// 不推荐
Content-Security-Policy: '*'
// 推荐
Content-Security-Policy: script-src 'self' https://apis.example.com
Why?
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.
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头
Electron 会处理 Content-Security-Policy
HTTP 标头,它可以在 webRequest.onHeadersReceived
中进行设置:
const { session } = require('electron')
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {...details.responseHeaders,'Content-Security-Policy': ['default-src 'none'']
}
})
})
CSP HTTP Header
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'']
}
})
})
CSP元标签
CSP的首选传递机制是HTTP报头,但是在使用file://
协议加载资源时,不可能使用此方法。 It can be useful in some cases, such as using the file://
protocol, 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'">
CSP Meta Tag
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, such as using the file://
protocol, 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'">
webRequest.onHeadersReceived([filter, ]listener)
webRequest.onHeadersReceived([filter, ]listener)
7) 不要设置allowRunningInsecureContent
为true
Electron的默认值即是建议值。
默认情况下,Electron不允许网站在HTTPS
中加载或执行非安全源(HTTP
) 中的脚本代码、CSS或插件。 将allowRunningInsecureContent
属性设为true
将禁用这种保护。
当网站的初始内容通过HTTPS
加载并尝试在子请求中加载HTTP
的资源时,这被称为"混合内容"。
7) Do Not Set allowRunningInsecureContent
to true
Recommendation is Electron's default
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.
Loading the initial HTML of a website over HTTPS
and attempting to load subsequent resources via HTTP
is also known as "mixed content".
为什么?
通过HTTPS
加载会将该资源进行加密传输,以保证其真实性和完整性。 参看只显示安全内容这节以获得更多信息。
Why?
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.
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
// 推荐
const mainWindow = new BrowserWindow({})
How?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
// Good
const mainWindow = new BrowserWindow({})
8) 不要开启实验室特性
Electron的默认值即是建议值。
Electron 的熟练用户可以通过 experimentalFeatures
属性来启用 Chromium 实验性功能。
8) Do Not Enable Experimental Features
Recommendation is Electron's default
Advanced users of Electron can enable experimental Chromium features using the experimentalFeatures
property.
为什么?
实验室特性,恰如其名,是实验性质且不对所有Chromium用户开启。更进一步说,这些特性对Electron的整体影响可能没有测试。
尽管存在合理的使用场景,但是除非你知道你自己在干什么,否则你不应该开启这个属性。
Why?
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.
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
// 推荐
const mainWindow = new BrowserWindow({})
How?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
// Good
const mainWindow = new BrowserWindow({})
9) 不要使用enableBlinkFeatures
Electron的默认值即是建议值。
Blink是Chromium里的渲染引擎名称。 就像experimentalFeatures
一样,enableBlinkFeatures
属性将使开发者启用被默认禁用的特性。
9) Do Not Use enableBlinkFeatures
Recommendation is Electron's default
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.
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: ['ExecCommandInJavaScript']
}
})
// 推荐
const mainWindow = new BrowserWindow()
How?
// Bad
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
// Good
const mainWindow = new BrowserWindow()
10) 不要使用allowpopups
Electron的默认值即是建议值。
如果您正在使用 <webview>
,您可能需要页面和脚本加载进您的 <webview>
标签以打开新窗口。 开启allowpopups
属性将使得BrowserWindows
可以通过window.open()
方法创建。 否则, <webview>
标签内不允许创建新窗口。
10) Do Not Use allowpopups
Recommendation is Electron's default
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.
为什么?
如果你不需要弹窗,最好使用默认值以关闭新BrowserWindows
的创建。 以下是最低的权限要求原则:若非必要,不要再网站中创建新窗口。
Why?
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.
怎么做?
<!-- 不推荐 -->
<webview allowpopups src="page.html"></webview>
<!-- 推荐 -->
<webview src="page.html"></webview>
How?
<!-- Bad -->
<webview allowpopups src="page.html"></webview>
<!-- Good -->
<webview src="page.html"></webview>
11) 创建WebView前确认其选项
通过渲染进程创建的WebView是不开启Node.js集成的,且也不能由自身开启。 但是,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.
11) Verify WebView Options Before Creation
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
.
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.
为什么?
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 可以让开发者关闭各种控制渲染进程的安全特性。 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.
Why?
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 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.
怎么做?
Before a <webview>
tag is attached, Electron will fire the will-attach-webview
event on the hosting webContents
. 利用这个事件来阻止可能含有不安全选项的 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
delete webPreferences.preloadURL
// Disable Node.js integration
webPreferences.nodeIntegration = false
// Verify URL being loaded
if (!params.src.startsWith('https://example.com/')) {event.preventDefault()
}
})
})
强调一下,这份列表只是将风险降到最低,并不会完全屏蔽风险。 如果您的目的是展示一个网站,浏览器将是一个更安全的选择。
How?
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
delete webPreferences.preloadURL
// 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, it does not remove it. If your goal is to display a website, a browser will be a more secure option.
12) 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.
12) 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.
为什么?
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.
Why?
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.
怎么做?
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.
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 URL = require('url').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()
}
})
})
How?
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.
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 URL = require('url').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()
}
})
})
13) 禁用或限制新窗口的创建
If you have a known set of windows, it's a good idea to limit the creation of additional windows in your app.
13) 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.
为什么?
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.
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.
Why?
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.
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.
怎么做?
webContents
will emit the new-window
event before creating new windows. That event will be passed, amongst other parameters, the url
the window was requested to open and the options used to create it. We recommend that you use the event to scrutinize the creation of windows, limiting it to only what you need.
const { shell } = require('electron')
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', async (event, navigationUrl) => {
// In this example, we'll ask the operating system
// to open this event's url in the default browser.
event.preventDefault()
await shell.openExternal(navigationUrl)
})
})
How?
webContents
will emit the new-window
event before creating new windows. That event will be passed, amongst other parameters, the url
the window was requested to open and the options used to create it. We recommend that you use the event to scrutinize the creation of windows, limiting it to only what you need.
const { shell } = require('electron')
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', async (event, navigationUrl) => {
// In this example, we'll ask the operating system
// to open this event's url in the default browser.
event.preventDefault()
await shell.openExternal(navigationUrl)
})
})
14) Do not use openExternal
with untrusted content
Shell's openExternal
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.
14) Do not use openExternal
with untrusted content
Shell's openExternal
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.
为什么?
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.
Why?
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.
怎么做?
// Bad
const { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
// Good
const { shell } = require('electron')
shell.openExternal('https://example.com/index.html')
How?
// Bad
const { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
// Good
const { shell } = require('electron')
shell.openExternal('https://example.com/index.html')
15) Disable the remote
module
The remote
module provides a way for the renderer processes to access APIs normally only available in the main process. Using it, a renderer can invoke methods of a main process object without explicitly sending inter-process messages. If your desktop application does not run untrusted content, this can be a useful way to have your renderer processes access and work with modules that are only available to the main process, such as GUI-related modules (dialogs, menus, etc.).
However, if your app can run untrusted content and even if you sandbox your renderer processes accordingly, the remote
module makes it easy for malicious code to escape the sandbox and have access to system resources via the higher privileges of the main process. Therefore, it should be disabled in such circumstances.
15) Disable the remote
module
The remote
module provides a way for the renderer processes to access APIs normally only available in the main process. Using it, a renderer can invoke methods of a main process object without explicitly sending inter-process messages. If your desktop application does not run untrusted content, this can be a useful way to have your renderer processes access and work with modules that are only available to the main process, such as GUI-related modules (dialogs, menus, etc.).
However, if your app can run untrusted content and even if you sandbox your renderer processes accordingly, the remote
module makes it easy for malicious code to escape the sandbox and have access to system resources via the higher privileges of the main process. Therefore, it should be disabled in such circumstances.
为什么?
remote
uses an internal IPC channel to communicate with the main process. "Prototype pollution" attacks can grant malicious code access to the internal IPC channel, which can then be used to escape the sandbox by mimicking remote
IPC messages and getting access to main process modules running with higher privileges.
Additionally, it's possible for preload scripts to accidentally leak modules to a sandboxed renderer. Leaking remote
arms malicious code with a multitude of main process modules with which to perform an attack.
Disabling the remote
module eliminates these attack vectors. Enabling context isolation also prevents the "prototype pollution" attacks from succeeding.
Why?
remote
uses an internal IPC channel to communicate with the main process. "Prototype pollution" attacks can grant malicious code access to the internal IPC channel, which can then be used to escape the sandbox by mimicking remote
IPC messages and getting access to main process modules running with higher privileges.
Additionally, it's possible for preload scripts to accidentally leak modules to a sandboxed renderer. Leaking remote
arms malicious code with a multitude of main process modules with which to perform an attack.
Disabling the remote
module eliminates these attack vectors. Enabling context isolation also prevents the "prototype pollution" attacks from succeeding.
怎么做?
// Bad if the renderer can run untrusted content
const mainWindow = new BrowserWindow({})
// Good
const mainWindow = new BrowserWindow({
webPreferences: {
enableRemoteModule: false
}
})
<!-- Bad if the renderer can run untrusted content -->
<webview src="page.html"></webview>
<!-- Good -->
<webview enableremotemodule="false" src="page.html"></webview>
How?
// Bad if the renderer can run untrusted content
const mainWindow = new BrowserWindow({})
// Good
const mainWindow = new BrowserWindow({
webPreferences: {
enableRemoteModule: false
}
})
<!-- Bad if the renderer can run untrusted content -->
<webview src="page.html"></webview>
<!-- Good -->
<webview enableremotemodule="false" src="page.html"></webview>
16) Filter the remote
module
If you cannot disable the remote
module, you should filter the globals, Node, and Electron modules (so-called built-ins) accessible via remote
that your application does not require. This can be done by blocking certain modules entirely and by replacing others with proxies that expose only the functionality that your app needs.
16) Filter the remote
module
If you cannot disable the remote
module, you should filter the globals, Node, and Electron modules (so-called built-ins) accessible via remote
that your application does not require. This can be done by blocking certain modules entirely and by replacing others with proxies that expose only the functionality that your app needs.
为什么?
Due to the system access privileges of the main process, functionality provided by the main process modules may be dangerous in the hands of malicious code running in a compromised renderer process. By limiting the set of accessible modules to the minimum that your app needs and filtering out the others, you reduce the toolset that malicious code can use to attack the system.
Note that the safest option is to fully disable the remote module. If you choose to filter access rather than completely disable the module, you must be very careful to ensure that no escalation of privilege is possible through the modules you allow past the filter.
Why?
Due to the system access privileges of the main process, functionality provided by the main process modules may be dangerous in the hands of malicious code running in a compromised renderer process. By limiting the set of accessible modules to the minimum that your app needs and filtering out the others, you reduce the toolset that malicious code can use to attack the system.
Note that the safest option is to fully disable the remote module. If you choose to filter access rather than completely disable the module, you must be very careful to ensure that no escalation of privilege is possible through the modules you allow past the filter.
怎么做?
const readOnlyFsProxy = require(/* ... */) // exposes only file read functionality
const allowedModules = new Set(['crypto'])
const proxiedModules = new Map(['fs', readOnlyFsProxy])
const allowedElectronModules = new Set(['shell'])
const allowedGlobals = new Set()
app.on('remote-require', (event, webContents, moduleName) => {
if (proxiedModules.has(moduleName)) {
event.returnValue = proxiedModules.get(moduleName)
}
if (!allowedModules.has(moduleName)) {
event.preventDefault()
}
})
app.on('remote-get-builtin', (event, webContents, moduleName) => {
if (!allowedElectronModules.has(moduleName)) {
event.preventDefault()
}
})
app.on('remote-get-global', (event, webContents, globalName) => {
if (!allowedGlobals.has(globalName)) {
event.preventDefault()
}
})
app.on('remote-get-current-window', (event, webContents) => {
event.preventDefault()
})
app.on('remote-get-current-web-contents', (event, webContents) => {
event.preventDefault()
})
app.on('remote-get-guest-web-contents', (event, webContents, guestWebContents) => {
event.preventDefault()
})
How?
const readOnlyFsProxy = require(/* ... */) // exposes only file read functionality
const allowedModules = new Set(['crypto'])
const proxiedModules = new Map(['fs', readOnlyFsProxy])
const allowedElectronModules = new Set(['shell'])
const allowedGlobals = new Set()
app.on('remote-require', (event, webContents, moduleName) => {
if (proxiedModules.has(moduleName)) {
event.returnValue = proxiedModules.get(moduleName)
}
if (!allowedModules.has(moduleName)) {
event.preventDefault()
}
})
app.on('remote-get-builtin', (event, webContents, moduleName) => {
if (!allowedElectronModules.has(moduleName)) {
event.preventDefault()
}
})
app.on('remote-get-global', (event, webContents, globalName) => {
if (!allowedGlobals.has(globalName)) {
event.preventDefault()
}
})
app.on('remote-get-current-window', (event, webContents) => {
event.preventDefault()
})
app.on('remote-get-current-web-contents', (event, webContents) => {
event.preventDefault()
})
app.on('remote-get-guest-web-contents', (event, webContents, guestWebContents) => {
event.preventDefault()
})