安全
有关如何正确上报 Electron 漏洞的信息,参阅 SECURITY.md.
对于上游 Chromium 漏洞: Electron 用其他版本的 Chromium 来更新。 For more information, see the Electron Release Timelines document.
前言
作为网络开发人员,我们通常喜欢浏览器的强大安全网,因为这样我们编写的代码风险较小。 我们的网站在沙盒中被赋予了有限的权力,我们相信我们的用户享受到的是一个由大型工程师团队打造的浏览器,它能够快速应对新发现的安全威胁。
当使用 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将造成更大的影响,因此非常建议你遵循安全软件开发最佳实践并进行安全性测试。
隔离不受信任的内容
每当你从不被信任的来源(如一个远程服务器)获取代码并在本地执行,其中就存在安全性问题。 As an example, consider a remote website being displayed inside a default 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
为 true - 不要开启实验性功能
- 不要使用
enableBlinkFeatures
<webview>
:不要使用allowpopups
<webview>
:验证选项与参数- 禁用或限制网页跳转
- 禁用或限制新窗口创建
- 不要对不可信的内容使用
shell.openExternal
- 使用当前版本的 Electron
- 验证所有 IPC 消息的
sender
- 不要使用
file://
,而是使用通用协议 - 检查你可以更换的Fuses
如果你想要自动检测错误的配置或是不安全的模式,可以使用electronegativity 有关使用 Electron 开发应用程序时的潜在弱点和实施错误,您可以参考开发人员和审计人员指南。
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 以来的默认行为。
加载远程内容时,在任何的呈现器(BrowserWindow
、WebContentsView
或 <webview>
)中禁止 Node.js 集成极为重要。 其目的是限制您授予远程内容的权限, 从而使攻击者在您的网站上执行 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
也 必须 开启.
For more information on what contextIsolation
is and how to enable it please see our dedicated Context Isolation document.
4. 启用进程沙盒化
沙盒 是一项 Chromium 功能,它使用操作系统来显著地限制渲染器进程可以访问的内容。 您应该在所有渲染器中启用沙盒。 不建议在一个未启动沙盒的进程(包括主进程)中加载、阅读或处理任何不信任的内容。
For more information on what Process Sandboxing is and how to enable it please see our dedicated Process Sandboxing document.
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 的默认值。
在渲染进程(BrowserWindow
、WebContentsView
或<webview>
)中禁用webSecurity
将会使重要的安全特性失效。
不要在生产环境中禁用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 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 meta tag
CSP 的首选传输机制是一个 HTTP 头. 但是, 使用 file://
协议加载资源时,无法使用此方法。 在某些情况下,使用 <meta>
标记直接在标记(makeup)中对页面设置策略 很有用:
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
8. 不要设置 allowRunningInsecureContent
为 true
此建议是 Electron 的默认值。
默认情况下,Electron不允许网站在HTTPS
中加载或执行非安全源(HTTP
) 中的脚本代码、CSS或插件。 将allowRunningInsecureContent
属性设为true
将禁用这种保护。
当网站的初始内容通过HTTPS
加载并尝试在子请求中加载HTTP
的资源时,这被称为"混合内容"。
为什么?
通过HTTPS
加载会将该资源进行加密传输,以保证其真实性和完整性。 参看只显示安全内容这节以获得更多信息。
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
// 推荐
const mainWindow = new BrowserWindow({})
9. 不要开启实验性功能
此建议是 Electron 的默认值。
Electron 的熟练用户可以通过 experimentalFeatures
属性来启用 Chromium 实验性功能。
为什么?
如名称所示,实验性功能是实验性的,尚未对所有 Chromium 用户启用。 此外,它们对整个 Electron 的影响很可能没有经过测试。
尽管存在合理的使用场景,但是除非你知道你自己在干什么,否则你不应该开启这个属性。
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
// 推荐
const mainWindow = new BrowserWindow({})
10. 不要使用enableBlinkFeatures
此建议是 Electron 的默认值。
Blink是Chromium里的渲染引擎名称。 就像experimentalFeatures
一样,enableBlinkFeatures
属性将使开发者启用被默认禁用的特性。
为什么?
通常来说,某个特性默认不被开启肯定有其合理的原因。 针对特定特性的合理使用场景是存在的。 作为开发者,你应该非常明白你为何要开启它,有什么后果,以及对你应用安全性的影响。 在任何情况下都不应该推测性的开启特性。
怎么做?
// 不推荐
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
// 推荐
const mainWindow = new BrowserWindow()
11. 不要在 WebViews 中使用 allowpopups
此建议是 Electron 的默认值。
如果你正在使用<webview>
,你也许需要通过<webview>
标签里加载的页面和代码,去创建一个新窗口。 The allowpopups
attribute enables them to create new BrowserWindows
using the window.open()
method. 否则, <webview>
标签内不允许创建新窗口。
为什么?
If you do not need popups, you are better off not allowing the creation of new BrowserWindows
by default. 以下是最低的权限要求原则:若非必要,不要再网站中创建新窗口。
怎么做?
<!-- 不推荐 -->
<webview allowpopups src="page.html"></webview>
<!-- 推荐 -->
<webview src="page.html"></webview>
12. 创建WebView前确认其选项
通过渲染进程创建的WebView是不开启Node.js集成的,且也不能由自身开启。 但是,WebView可以通过其webPreferences
属性创建一个独立的渲染进程。
通过在主进程中创建<webview>
,并确认其webPreferences没有禁用安全特性是个不错的办法。
为什么?
由于 <webview>
存在在DOM中,因此即使Node继承被禁用,它也可以通过运行在您的 网站上的脚本创建它们。
Electron 可以让开发者关闭各种控制渲染进程的安全特性。 通常情况下,开发者并不需要关闭他们中的任何一种 - 因此你不应该允许创建不同配置的<webview>
标签
怎么做?
在 <webview>
标签生效前,Electron将调用webContents
的will-attach-webview
事件。 利用这个事件来阻止可能含有不安全选项的 webViews
创建。
app.on('web-contents-created', (event, contents) => {
contents.on('will-attach-webview', (event, webPreferences, params) => {
// 如果未使用,则删除预加载脚本或验证其位置是否合法
delete webPreferences.preload
// 禁用 Node.js 集成
webPreferences.nodeIntegration = false
// 验证正在加载的 URL
if (!params.src.startsWith('https://example.com/')) {
event.preventDefault()
}
})
})
同样,这个清单只是将风险降低到最低限度,但没有将其消除。 如果您的目标是展示一个网站,浏览器将是一个更安全的选择。
13. 禁用或限制网页跳转
如果你的应用不需要导航或只需要导航到已知页面,最好将导航完全限制在该已知范围内,不允许任何其他类型的导航。
为什么?
导航是一种常见的攻击媒介。 如果攻击者可以诱使你的应用导航离开其当前页面,则他们可能会强制你的应用在 Internet 上打开网站。 即使你的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 }) => {
// 在这个例子中,我们要求操作系统
// 在默认浏览器中打开此事件的URL
//
// 关于哪些URL应该被允许通过shell.openExternal打开,
// 请参照以下项目。
if (isSafeForExternalOpen(url)) {
setImmediate(() => {
shell.openExternal(url)
})
}
return { action: 'deny' }
})
})
15. 不要对不可信的内容使用 shell.openExternal
shell 模块的 openExternal
API 能让你使用本地桌面程序打开指定的协议 URI。 例如,在 macOS 上,此功能与 open
终端命令实用程序类似,将基于 URI 和文件类型关联打开特定的应用程序。
为什么?
不正确使用openExternal
可能会危及用户的主机。 当 openExternal 使用内容不受信任时,它可以用来执行任意命令。
怎么做?
// 不好
const { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
// 好
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),你的应用程序将更加安全,因为潜在的安全问题并不广为人知。
怎么做?
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
应始终验证传入的 IPC 消息的 sender
属性,确保 未使用不受信任的渲染器执行动作或向不受信任的渲染器发送信息。
为什么?
从理论上讲,所有 Web Frame 都可以将 IPC 消息发送到主进程,包括在某些情况下 iframe 和子窗口。 如果您的 IPC 消息通过 event.reply
向发件人返回 用户数据,或者执行了渲染器 无法本机执行的特权操作,则应确保您没有侦听第三方 web frame。
您应该默认验证 所有 IPC 消息 sender
。
怎么做?
// 不好的做法
ipcMain.handle('get-secrets', () => {
return getSecrets()
})
// 好的做法
ipcMain.handle('get-secrets', (e) => {
if (!validateSender(e.senderFrame)) return null
return getSecrets()
})
function validateSender (frame) {
// 使用实际的URL解析器和白名单来评估URL的主机
if ((new URL(frame.url)).host === 'electronjs.org') return true
return false
}
18.不要使用file://
,而是使用通用协议 不要使用file://
,而是使用通用协议
您应该使用通用协议提供本地页面,而非file://
协议。
为什么?
相较于网络浏览器,file://
协议在Electron中将获得更多权限。甚至在浏览器中,它的处理方式也与 http/https URL 不同。 使用通过协议更符合经典的网络 url 行为,同时能更好地控制加载的内容和时间。
在file://
上运行的页面可以单方面访问您机器上的每个文件,这意味着可以利用 XSS 问题加载用户机器上的任意文件。 使用通用协议可以防止这类问题,因为您可以将协议限制为只为特定的一组文件提供服务。
怎么做?
通过protocol.handle
可以了解到如何通过通用协议提供文件/内容
19. 检查你可以更换的Fuses
Electron ships提供了很多有用的选项,但很大一部分的应用程序可能不需要。 为了避免自行创建版本,这些Fuses可以被关闭或使用
为什么?
例如,从命令行运行特定环境变量或 CLI 参数时,runAsNode
和 nodeCliInspect
允许应用程序有不同的行为。 它们可以通过您的应用程序来执行设备上的命令。
这让您的应用程序可能有权限运行一些原本不被允许运行的外部脚本。
怎么做?
为了更方便地使用这些fuses,我们制作了@electron/fuses
模块。 如需使用详情和潜在错误情况,请查看该模块的 README,并参考文档中的How do I flip the fuses?。