0x00背景
如果你可以建一个网站,你就可以建一个桌面应用程序。 Electron 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可。
简单来说,Electron 基于 Chromium 和 Node.js, 让你可以使用 HTML, CSS 和 JavaScript 构建应用。把一个浏览器的标签视图去掉,并且只限定访问特定的网页。
0x01 安全性,原生能力和你的责任
Web开发人员通常享有浏览器强大的网络安全特性,而自己的代码风险相对较小。 我们的网站在沙盒中被授予有限的权限,我们相信用户会喜欢由大型工程师团队构建的浏览器,该浏览器能够快速响应新发现的安全威胁。
当使用 Electron 时,很重要的一点是要理解 Electron 不是一个 Web 浏览器。 它允许您使用熟悉的 Web 技术构建功能丰富的桌面应用程序,但是您的代码具有更强大的功能。 JavaScript 可以访问文件系统,用户 shell 等。 这允许您构建更高质量的本机应用程序,但是内在的安全风险会随着授予您的代码的额外权力而增加。
考虑到这一点,请注意,展示任意来自不受信任源的内容都将会带来严重的安全风险,而这种风险Electron也没打算处理。 事实上,最流行的 Electron 应用程序(Atom,Slack,Visual Studio Code 等) 主要显示本地内容(即使有远程内容也是无 Node 的、受信任的、安全的内容) - 如果您的应用程序要运行在线的源代码,那么您需要确保源代码不是恶意的。
报告安全问题
有关如何正确上报 Electron 漏洞的信息,参阅 SECURITY.md
Chromium 安全问题和升级
Electron和Chromium同步更新。 有关更多信息,请参见《 Electron Release Cadence》博客文章。
安全是所有人的共同责任
重要的是要记住,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代码 如果你想要显示远程内容,请使用 <webview> Tag或者 BrowserView,并确保禁用 nodeIntegration 并启用 contextIsolation
Electron 安全警告
从Electron 2.0版本开始,开发者将会在开发者控制台看到打印的警告和建议。 这些警告仅在可执行文件名为 Electron 时才会为开发者显示。
你可以通过在process.env 或 window对象上配置ELECTRON_ENABLE_SECURITY_WARNINGS 或ELECTRON_DISABLE_SECURITY_WARNINGS来强制开启或关闭这些警告。
0x02 清单:安全建议
为加强程序安全性,你至少应当遵循下列规则:
- 只加载安全的内容
- 禁止在所有渲染器中使用Node.js集成显示远程内容
- 做所有显示远程内容的渲染器中启用上下文隔离。
- 在所有加载远程内容的会话中使用 ses.setPermissionRequestHandler().
- 不要禁用 webSecurity
- 定义一个Content-Security-Policy并设置限制规则(如:script-src 'self')
- 不要设置 allowRunningInsecureContent 为 true.
- 不要开启实验性功能
- 不要使用enableBlinkFeatures
- <webview>:不要使用 allowpopups
- <webview>:验证选项与参数
- 禁用或限制网页跳转
- 禁用或限制新窗口创建
- 不要对不可信的内容使用 openExternal
- 禁用 remote 模块
- 限制 remote 模块
- 使用当前版本的 Electron
如果你想要自动检测错误的配置或是不安全的模式,可以使用electronegativity 关于在使用Electron进行应用程序开发中的潜在薄弱点或者bug,您可以参考开发者与审核人员指南
1) 仅加载安全内容
任何不属于你的应用的资源都应该使用像HTTPS这样的安全协议来加载。 换言之, 不要使用不安全的协议 (如 HTTP)。 同理,我们建议使用WSS,避免使用WS,建议使用FTPS ,避免使用FTP,等等诸如此类的协议。
为什么?
HTTPS 有三个主要好处:
1) 它对远程服务器进行身份验证, 确保您的应用程序连接到正确的主机而不是模仿器。 2) 确保数据完整性, 断言数据在应用程序和主机之间传输时未被修改。 3) 它对用户和目标主机之间的通信进行加密, 从而更难窃听应用程序和主机之间发送的信息。
怎么做?
// 不推荐
browserWindow.loadURL ('http://example.com')// 推荐
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 集成
This recommendation is the default behavior in Electron since 5.0.0.
加载远程内容时,不论使用的是哪一种渲染器(BrowserWindow,BrowserView 或者 <webview>),最重要的就是绝对不要启用 Node.js 集成。 其目的是限制您授予远程内容的权限, 从而使攻击者在您的网站上执行 JavaScript 时更难伤害您的用户。
在此之后,你可以为指定的主机授予附加权限。 举例来说,如果你正在打开一个指向 https://example.com/ 的 BrowserWindow,那么你可以给他刚刚好足够的权限,但是绝对不要超出这个范围。
为什么?
如果攻击者跳过渲染进程并在用户电脑上执行恶意代码,那么这种跨站脚本(XSS) 攻击的危害是非常大的。 跨站脚本攻击很常见,通常情况下,威力仅限于执行代码的网站。 禁用Node.js集成有助于防止XSS攻击升级为“远程代码执行” (RCE) 攻击。
怎么做?
// 不推荐const mainWindow = new BrowserWindow({
webPreferences: {
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')
<!-- Bad --><webview nodeIntegration src="page.html"></webview>
<!-- Good --><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
}
3) 为远程内容开启上下文隔离
上下文隔离是Electron的一个特性,它允许开发者在预加载脚本里运行代码,里面包含Electron API和专用的JavaScript上下文。 实际上,这意味全局对象如 Array.prototype.push 或 JSON.parse等无法被渲染进程里的运行脚本修改。
Electron使用了和Chromium相同的Content Scripts技术来开启这个行为。
即使您使用选项 nodeIntegration: false 进行强制隔离并防止其使用Node原语,contextIsolation 也必须被启用。
Why & How?
For more information on what contextIsolation is and how to enable it please see our dedicated Context Isolation document.
4) 处理来自远程内容的会话许可请求
当你使用Chromes时,也许见过这种许可请求:每当网站尝试使用某个特性时,就会弹出让用户手动确认(如网站通知)
此API基于Chromium permissions API,并已实现对应的许可类型。
为什么?
默认情况下,Electron将自动批准所有的许可请求,除非开发者手动配置一个自定义处理函数。 尽管默认如此,有安全意识的开发者可能希望默认反着来。
怎么做?
const { session } = require('electron')
session
.fromPartition('some-partition')
.setPermissionRequestHandler((webContents, permission, callback) => {
const url = webContents.getURL()
if (permission === 'notifications') {
// 通过许可请求
callback(true)
}
// Verify URL
if (!url.startsWith('https://example.com/')) {
// 拒绝许可请求
return callback(false)
}
})
5) 不要禁用WebSecurity
Electron的默认值就是建议值。
在渲染进程(BrowserWindow、BrowserView 和 <webview>)中禁用 webSecurity 将导致至关重要的安全性功能被关闭。
不要在生产环境中禁用webSecurity。
为什么?
禁用 webSecurity 将会禁止同源策略并且将 allowRunningInsecureContent 属性置 true。 换句话说,这将使得来自其他站点的非安全代码被执行。
怎么做?
// 不推荐const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false
}
})
// 推荐const mainWindow = new BrowserWindow()
<!-- Bad --><webview disablewebsecurity src="page.html"></webview>
<!-- Good --><webview src="page.html"></webview>
6) 定义一个内容安全策略
内容安全策略(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.comEN
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元标签
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'">
7) 不要设置allowRunningInsecureContent为true
Electron的默认值就是建议值。
默认情况下,Electron不允许网站在HTTPS中加载或执行非安全源(HTTP) 中的脚本代码、CSS或插件。 将allowRunningInsecureContent属性设为true将禁用这种保护。
当网站的初始内容通过HTTPS加载并尝试在子请求中加载HTTP的资源时,这被称为"混合内容"。
为什么?
通过HTTPS加载会将该资源进行加密传输,以保证其真实性和完整性。 参看只显示安全内容这节以获得更多信息。
怎么做?
// 不推荐const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
// 推荐const mainWindow = new BrowserWindow({})
8) 不要开启实验室特性
Electron的默认值就是建议值。
Electron 的熟练用户可以通过 experimentalFeatures 属性来启用 Chromium 实验性功能。
为什么?
尽管存在合理的使用场景,但是除非你知道你自己在干什么,否则你不应该开启这个属性。
怎么做?
// 不推荐const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
// 推荐const mainWindow = new BrowserWindow({})
9) 不要使用enableBlinkFeatures
Electron的默认值就是建议值。
Blink是Chromium里的渲染引擎名称。 就像experimentalFeatures一样,enableBlinkFeatures属性将使开发者启用被默认禁用的特性。
为什么?
通常来说,某个特性默认不被开启肯定有其合理的原因。 针对特定特性的合理使用场景是存在的。 作为开发者,你应该非常明白你为何要开启它,有什么后果,以及对你应用安全性的影响。 在任何情况下都不应该推测性的开启特性。
怎么做?
// 不推荐const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
// 推荐const mainWindow = new BrowserWindow()EN
10) 不要使用allowpopups
Electron的默认值就是建议值。
如果您正在使用 <webview> ,您可能需要页面和脚本加载进您的 <webview> 标签以打开新窗口。 开启allowpopups属性将使得BrowserWindows可以通过window.open()方法创建。 否则, <webview> 标签内不允许创建新窗口。
为什么?
如果你不需要弹窗,最好使用默认值以关闭新BrowserWindows的创建。 以下是最低的权限要求原则:若非必要,不要再网站中创建新窗口。
怎么做?
<!-- Bad --><webview allowpopups src="page.html"></webview>
<!-- Good --><webview src="page.html"></webview>EN
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.
为什么?
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>标签
怎么做?
在 <webview>标签生效前,Electron将产生一个will-attach-webview事件到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()
}
})
})
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) 禁用或限制导航
如果您的应用程序不需要导航或仅需要导航到已知页面,则最好将导航完全限制在该已知范围内,而不允许任何其他类型的导航。
为什么?
导航是一种常见的攻击手段。 如果攻击者可以说服您的应用程序离开当前页面,则可能会迫使您的应用程序打开Internet上的网站。 即使您将WebContents配置为更安全(例如禁用了nodeIntegration或启用了contextIsolation),让您的应用程序打开随机网站也会使开发应用程序的工作变得更加容易。
常见的攻击模式是,攻击者诱使您的应用程序用户与该应用程序进行交互,以使其导航至攻击者的页面之一。 通常通过链接,插件或其他用户生成的内容来完成此操作。
怎么做?
如果您的应用程序不需要导航,则可以在will-navigate处理程序中调用event.preventDefault()。 如果您知道您的应用可能会导航到哪些页面,请检查事件处理程序中的URL,仅在导航与您期望的URL匹配时才进行导航。
我们建议您对URL使用Node的解析器。 有时可能会愚弄简单的字符串比较-startsWith('https://example.com')测试会让https://example.com.attacker.com通过。
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) 禁用或限制新窗口的创建
如果您拥有一组已知的窗口,则最好限制在应用程序中创建其他窗口。
为什么?
就像导航一样,创建新的webContents是常见的攻击手段。 攻击者试图说服您的应用以比以往更多的特权来创建新的窗口,框架或其他渲染器进程。 或打开了以前无法打开的页面。
如果除了您需要创建的窗口之外,您还不需要创建其他窗口,则禁用创建可以免费为您提供额外的安全性。 对于打开一个``浏览器窗口''并且无需在运行时打开任意数量的其他窗口的应用程序来说通常是这种情况。
怎么做?
webContents将在创建新窗口之前发出new-window事件。 除其他参数外,还将传递该事件,请求打开窗口的URL和用于创建该事件的选项。 我们建议您使用事件检查窗口的创建,将其限制为仅您需要的。
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) 不要使用带有不受信任内容的“ openExternal”
Shell的openExternal允许使用桌面的本机实用程序打开给定的协议URI。 例如,在macOS上,此功能类似于openterminal命令实用程序,并将基于URI和文件类型关联打开特定的应用程序。
为什么?
可能会滥用openExternal来损害用户的主机。 当openExternal与不受信任的内容一起使用时,可以利用它来执行任意命令。
怎么做?
// Badconst { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
// Goodconst { shell } = require('electron')
shell.openExternal('https://example.com/index.html')
15) 禁用“远程”模块
远程模块为渲染器进程提供了一种访问通常仅在主进程中可用的API的方式。 使用它,渲染器可以调用主流程对象的方法,而无需显式发送进程间消息。 如果您的桌面应用程序不运行不受信任的内容,这可能是使渲染器进程访问并使用仅对主进程可用的模块(例如与GUI相关的模块(对话框,菜单等))的有用方法。
但是,如果您的应用程序可以运行不受信任的内容,并且即使您相应地对沙盒渲染器进程进行了处理,则远程模块也可以使恶意代码更容易逃脱沙盒并通过主进程的更高特权来访问系统资源。 因此,在这种情况下应禁用它。
为什么?
远程使用内部IPC通道与主进程进行通信。 ``原型污染''攻击可以授予恶意代码访问内部IPC通道的权限,然后可以通过模仿远程IPC消息并访问以更高特权运行的主要流程模块来逃脱沙箱。
此外,预加载脚本可能会将模块意外泄漏到沙盒渲染器。 远程泄漏可将恶意代码与大量主要流程模块结合在一起,以执行攻击。
禁用远程模块会消除这些攻击媒介。 启用上下文隔离还可以阻止“原型污染”攻击的成功。
怎么做?
// Bad if the renderer can run untrusted contentconst mainWindow = new BrowserWindow({})
// Goodconst 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) 过滤“远程”模块
如果无法禁用远程模块,则应过滤可通过应用程序不需要的远程访问的全局,节点和电子模块(所谓的内置模块)。 可以通过完全阻止某些模块并将其替换为仅暴露应用程序所需功能的代理来完成。
为什么?
由于主进程具有系统访问特权,因此,在受损的渲染器进程中运行的恶意代码手中,由主进程模块提供的功能可能很危险。 通过将可访问模块的数量限制为应用程序所需的最少数量并过滤掉其他模块,可以减少恶意代码可用于攻击系统的工具集。
请注意,最安全的选择是完全禁用远程模块。 如果选择过滤访问而不是完全禁用模块,则必须非常小心以确保通过允许通过过滤器的模块无法进行特权升级。
怎么做?
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()
})
17) 使用当前版本的 Electron
您应该争取始终使用最新版本的Electron。 每当发布新的主要版本时,您都应尝试尽快更新您的应用。
为什么?
与使用较新版本的那些组件的应用程序相比,使用较早版本的Electron,Chromium和Node.js构建的应用程序更容易成为目标。 一般而言,较旧版本的Chromium和Node.js的安全性问题和漏洞利用更为广泛。
Chromium和Node.js都是数以千计的优秀开发人员构建的令人印象深刻的工程壮举。 鉴于它们的受欢迎程度,它们的安全性由同样熟练的安全研究人员进行了仔细的测试和分析。 这些研究人员中有许多负责任地披露了漏洞,这通常意味着研究人员将在发布问题之前给Chromium和Node.js一些时间来修复问题。 如果您的应用程序运行的是最新版本的Electron(因此是Chromium和Node.js),而该版本的潜在安全问题并未广为人知,则您的应用程序将更加安全。
0x03 参考文章
https://myslide.cn/slides/14368
https://www.jianshu.com/p/97179e311cea