Electron使用preload预加载及安全策略
使用 Electron 很重要的一点是要理解 Electron 不是一个 Web 浏览器。它允许您使用熟悉的 Web 技术构建功能丰富的桌面应用程序,但是您的代码具有更强大的功能。JavaScript 可以访问文件系统,用户 shell 等。这允许您构建更高质量的本机应用程序,但是内在的安全风险会随着授予您的代码的额外权力而增加。
使用 Electron 很重要的一点是要理解 Electron 不是一个 Web 浏览器。 它允许您使用熟悉的 Web 技术构建功能丰富的桌面应用程序,但是您的代码具有更强大的功能。 JavaScript 可以访问文件系统,用户 shell 等。 这允许您构建更高质量的本机应用程序,但是内在的安全风险会随着授予您的代码的额外权力而增加。
事实上,最流行的 Electron 应用程序(Atom,Slack,Visual Studio Code 等) 主要显示本地内容(即使有远程内容也是无 Node 的、受信任的、安全的内容) - 如果您的应用程序要运行在线的源代码,那么您需要确保源代码不是恶意的。
一、preload
preload是electron的预加载机制,可以理解为在electron创建时将nodejs环境加载到渲染进程中使用。程序的进程是相互独立的,electron中渲染进程和主进程的协同工作一般采用IPC进程通信或者在渲染进程中集成node环境,还有早期比较低效的remote模块方式使用node环境。除非保证渲染进程的JavaScript都是可信安全的,否则不推荐在渲染进程集成node环境。使用preload的目的是在electron中不开启node环境集成情况下使用node的模块,避免不安全的JavaScript代码随意使用node环境。
preload的工作是在创建窗体时预加载需要node模块到渲染进程,然后以API方式暴露给渲染进程调用,preload共享渲染进程的window、document对象,因此preload可以轻松操作DOM,而渲染进程不共享preload的global对象。
preload.js
const { contextBridge, ipcRenderer} = require('electron');
const fs = require('fs');
contextBridge.exposeInMainWorld('fsApi', {
writeFile: (filename, text, callback) => {
fs.writeFile(filename, text, callback);
},
readFile: (filename, encode, callback) => {
fs.readFile(filename, encode, callback);
},
unlink: (filename, callback) => {
fs.unlink(filename, callback);
}
});
contextBridge.exposeInMainWorld('ipcRendererApi', {
send: (channel, args) => ipcRenderer.send(channel, args),
once: (channel, listener) => ipcRenderer.once(channel, listener),
on: (channel, listener) => ipcRenderer.on(channel, listener),
});
调用预加载API
ipcRendererApi.send('close', args)
二、安全策略
(一)webPreferences属性
Electron安全策略几乎都在webPreferences属性中设置,通过属性来开启或者关闭渲染进程相关安全选项。
属性 | 值 | 默认 | 作用 | 说明 |
---|---|---|---|---|
nodeIntegration 或 nodeIntegrationInWorker | true/false | false | 渲染进程集成node环境 | 默认关闭,当渲染进程加载远程内容时必须关闭,远程内容不确定性,避免跳过渲染进程在node环境下执行JS脚本。webview中同样不推荐使用nodeIntegration属性。<webview nodeIntegration src="page.html"></webview> |
preload | 预加载脚本 | 当禁用Node.js集成时,你依然可以暴露API给你的站点以使用Node.js的模块功能或特性。即预加载node模块到渲染进程使用,另外预加载的模块可以直接访问渲染进程的DOM。 electron早期继承remote模块,用于在渲染进程调用主进程的功能,当前最新的electron已经不是默认模块需要另外安装,使用remote模块相比较于IPC通信更低效,建议使用IPC与主进程通信。 | ||
contextIsolation | true/false | true | 开启JS上下文隔离 | 默认开启,即便使用了 nodeIntegration: false, 要实现真正的强隔离并且防止使用 Node.js 的功能, contextIsolation 也 必须 开启。一般情况下结合nodeIntegration一起使用。 |
webSecurity | true/false | true | 安全性功能 | 在渲染进程(BrowserWindow、BrowserView 和 <webview> )禁用webSecurity,这将使得来自其他站点的非安全代码被执行。 |
allowRunningInsecureContent | true/false | false | 允许运行不安全内容 | 默认情况下,Electron不允许网站在HTTPS中加载或执行非安全源(HTTP) 中的脚本代码、CSS或插件。 将allowRunningInsecureContent属性设为true将禁用这种保护。 |
experimentalFeatures | true/false | false | 实验性功能 | 实验性功能是实验性的,尚未对所有 Chromium 用户启用。 |
enableBlinkFeatures | Blink渲染引擎特性 | 通常来说,某个特性默认不被开启肯定有其合理的原因。 针对特定特性的合理使用场景是存在的。 作为开发者,你应该非常明白你为何要开启它,有什么后果,以及对你应用安全性的影响。 在任何情况下都不应该推测性的开启特性。 |
(二)加载安全内容
在渲染进程中只加载HTTPS的安全内容
main.js (Main Process)
// 不推荐
browserWindow.loadURL ('http://example.com')
// 推荐
browserWindow.loadURL ('https://example.com')
index.html (Renderer Process)
<!-- 不推荐 -->
<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">
(三)启用进程沙盒化
您应该在所有渲染器中启用沙盒。 不建议在一个未启动沙盒的进程(包括主进程)中加载、阅读或处理任何不信任的内容。Electron20及以后的版本默认开启沙盒,这导致preload加载nodejs的插件报错,在确保当前程序的代码安全性的前提下可以手动关闭沙盒。详情
(四)限制请求会话权限
通过session来过滤请求协议或者域名,electron该API基于Chromium permissions API实现。
main.js (Main Process)
const { session } = require('electron')
const URL = require('url').URL
session
.fromPartition('some-partition')
.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)
}
})
(五)Content-Security-Policy(CSP)
CSP是HTTP协议标头,限制允许渲染进程执行的脚本,是应对跨站脚本攻击和数据注入攻击的策略。
// 不推荐
Content-Security-Policy: '*'
// 推荐
Content-Security-Policy: script-src 'self' https://apis.example.com
main.js (Main Process)
const { session } = require('electron')
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src \'none\'']
}
})
})
index.html (Renderer Process)
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
仅适用于HTTP协议
(六)Webview的allowpopups属性
index.html (Renderer Process)
<!-- 不推荐 -->
<webview allowpopups src="page.html"></webview>
<!-- 推荐 -->
<webview src="page.html"></webview>
(七)Webview标签选项
渲染进程通过标签创建的webview没有node继承,而从主进程创建的webview可以加成node。在 标签生效前,Electron将产生一个will-attach-webview事件到webContents中。 利用这个事件来阻止可能含有不安全选项的 webViews 创建。
main.js (Main Process)
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()
}
})
})
(八)禁用或限制网页跳转
该功能主要是防止跳转到指定页面后执行JS,从而存在跨站点攻击隐患,即便是关闭node集成开启上下文隔离的情况下,也不应该允许任意网页跳转。
如果您的应用不需要导航,您可以在 will-navigate 处理器中调用 event.preventDefault()。 如果您知道您的应用程序可能会导航到哪些界面,请在事件处理器中检查URL,并且仅当它与您预期的URL匹配时才进行导航。
main.js (Main Process)
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()
}
})
})
(九)禁止或限制新窗口创建
当打开新窗口时,注册事件来处理即将打开新窗口的URL,从而过滤掉不安全的url打开新窗口。
main.js (Main Process)
const { 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' }
})
})
(十)shell.openExternal
shell.openExternal可以用来执行任意命令,不受信任的内容不要使用该API。
main.js (Main Process)
// 不好
const { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
// 好
const { shell } = require('electron')
shell.openExternal('https://example.com/index.html')
(十一)验证IPC消息发送者
任何窗口都可以向主进程发送消息,主进程回复渲染进程时,应该确定发送者身份。
main.js (Main Process)
// Bad
ipcMain.handle('get-secrets', () => {
return getSecrets();
});
// Good
ipcMain.handle('get-secrets', (e) => {
if (!validateSender(e.senderFrame)) return null;
return getSecrets();
});
function validateSender(frame) {
// Value 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;
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)