安全
有关如何正确上报 Electron 漏洞的信息,参阅 SECURITY.md.
对于上游 Chromium 漏洞: Electron 用其他版本的 Chromium 来更新。 欲了解更多信息,请参阅 Electron发布时间表 文档。
前言
作为网络开发人员,我们通常喜欢浏览器的强大安全网,因为这样我们编写的代码风险较小。 我们的网站在沙盒中被赋予了有限的权力,我们相信我们的用户享受到的是一个由大型工程师团队打造的浏览器,它能够快速应对新发现的安全威胁。
当使用 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提供了五百万可重用的软件包,而你应当承担起选择可信任的第三方库。 如果你使用了受已知漏洞的过时的库,或是依赖于维护的很糟糕的代码,你的程序安全就可能面临威胁。
-
遵循安全编码实践你的代码是你的程序安全的第一道防线。 一般的网络漏洞,例如跨站脚本攻击(Cross-Site Scripting, XSS),对Electron将造成更大的影响,因此非常建议你遵循安全软件开发最佳实践并进行安全性测试。
隔离不受信任的内容
每当你从不被信任的来源(如一个远程服务器)获取代码并在本地执行,其中就存在安全性问题。 例如在默认的 BrowserWindow
中显示一个远程网站. 如果攻击者以某种方式设法改变所述内容 (通过直接攻击源或者通过在应用和实际目的地之间进行攻击) ,他们将能够在用户的机器上执行本地代码。
:::警告
无论 如何,在启用Node.js集成的情况下,你都不该加载并执行远程代码。 相反,只使用本地文件(和您的应用打包在一起)来执行Node.js代码
:::
安全警告和建议被打印到开发者控制台。 只有当二进制文件的名称为Electron时,它们才会显示,这表明开发人员 当前正在查看控制台。
你可以通过在process.env
或 window
对象上配置ELECTRON_ENABLE_SECURITY_WARNINGS
或ELECTRON_DISABLE_SECURITY_WARNINGS
来强制开启或关闭这些警告。
清单:安全建议
为加强程序安全性,你至少应当遵循下列规则:
- 只加载安全的内容
- 禁止在所有渲染器中使用Node.js集成显示远程内容
- 在所有渲染器中启用上下文隔离
- 启用进程沙盒化
- 在所有加载远程内容的会话中使用
ses.setPermissionRequestHandler()
. - 不要禁用
webSecurity
- 定义一个
Content-Security-Policy
并设置限制规则(如:script-src 'self'
) - 不要设置
allowRunningInsecureContent
为 true - 不要开启实验性功能
- 不要使用
enableBlinkFeatures
<webview>
:不要使用allowpopups
<webview>
:验证选项与参数- 禁用或限制网页跳转
- 禁用或限制新窗口创建
- 不要对不可信的内容使用
shell.openExternal
- 使用当前版本的 Electron
- 验证所有 IPC 消息的
sender
- Check which fuses you can change
如果你想要自动检测错误的配置或是不安全的模式,可以使用electronegativity 关于在使用Electron进行应用程序开发中的潜在薄弱点或者bug,您可以参考开发者与审核人员指南.
1. 只加载安全的内容
任何不属于你的应用的资源都应该使用像HTTPS
这样的安全协议来加载。 换言之, 不要使用不安全的协议 (如 HTTP
)。 同理,我们建议使用WSS
,避免使用WS
,建议使用FTPS
,避免使用FTP
,等等诸如此类的协议。
为什么?
HTTPS
有两个主要好处:
- 确保数据完整性,断言数据在您的应用程序和主机之间传输时未被修改。
- 它会加密您的用户和目标主机之间的流量,使窃听应用与主机之间发送的信息变得更加困难。
怎么做?
// 不推荐
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">
2. 不要为远程内容启用 Node.js 集成
此建议是 Electron 自 5.0.0 以来的默认行为。
It is paramount that you do not enable Node.js integration in any renderer (BrowserWindow
, WebContentsView
, or <webview>
) that loads remote content. 其目的是限制您授予远程内容的权限, 从而使攻击者在您的网站上执行 JavaScript 时更难伤害您的用户。
在此之后,你可以为指定的主机授予附加权限。 举例来说,如果你正在打开一个指向 https://example.com/
的 BrowserWindow,那么你可以给他刚刚好足 够的权限,但是绝对不要超出这个范围。
为什么?
如果攻击者跳过渲染进程并在用户电脑上执行恶意代码,那么这种跨站脚本(XSS) 攻击的危害是非常大的。 跨站脚本攻击很常见,通常情况下,威力仅限于执行代码的网站。 禁用Node.js集成有助于防止XSS攻击升级为“远程代码执行” (RCE) 攻击。
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
})
mainWindow.loadURL('https://example.com')
// 推荐
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特性, 以使开发者可以通过contextBridge API向远程加载的内容公开自定义API。
3. 上下文隔离
此建议是 Electron 自 12.0.0 以来的默认行为。
上下文隔离是Electron的一个特性,它允许开发者在预加载脚本里运行代码,里面包含Electron API和专用的JavaScript上下文。 实际上,这 意味全局对象如 Array.prototype.push
或 JSON.parse
等无法被渲染进程里的运行脚本修改。
Electron使用了和Chromium相同的Content Scripts技术来开启这个行为。
即便使用了 nodeIntegration: false
, 要实现真正的强隔离并且防止使用 Node.js 的功能, contextIsolation
也 必须 开启.
获取关于contextIsolation
是什么以及如何使用的更多信息,请参阅我们的上下文隔离文档
4. 启用进程沙盒化
沙盒 是一项 Chromium 功能,它使用操作系统来显著地限制渲染器进程可以访问的内容。 您应该在所有渲染器中启用沙盒。 不建议在一个未启动沙盒的进程(包括主进程)中加载、阅读或处理任何不信任的内容。
欲了解更多有关进程沙盒的信息,以及如何启用它,请查看我们专门的 进程沙盒 文档。
5. 处理来自远程内容的会话许可的请求
当你使用Chrome时,也许见过这种许可请求:每当网站尝试使用某个特性时,就会弹出让用户手动确认(如网站通知)
此API基于Chromium permissions 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') {
// 批准权限请求
callback(true)
}
// 验证 URL
if (parsedUrl.protocol !== 'https:' || parsedUrl.host !== 'example.com') {
// 驳回权限请求
return callback(false)
}
})
6. 不要禁用 webSecurity
此建议是 Electron 的默认值。
You may have already guessed that disabling the webSecurity
property on a renderer process (BrowserWindow
, WebContentsView
, or <webview>
) disables crucial security features.
不要在生产环境中禁用webSecurity
。
为什么?
禁用 webSecurity
将会禁止同源策略并且将 allowRunningInsecureContent
属性置 true
。 换句话说,这将使得来自其他站点的非安全代码被执行。
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false
}
})
// 推荐
const mainWindow = new BrowserWindow()
<!-- 不推荐 -->
<webview disablewebsecurity src="page.html"></webview>
<!-- 推荐 -->
<webview src="page.html"></webview>
7. Content Security Policy(内容安全策略)
内容安全策略(CSP) 是应对跨站脚本攻击和数据注入攻击的又一层保护措施。 我们建议任何载入到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
CSP HTTP headers
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\'']
}
})
})