Electron Security
基础概念
Electron 是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。通过在其二进制文件中嵌入 Chromium 和 Node.js,Electron 允许你维护一个 JavaScript 代码库,并创建可在 Windows、macOS 和 Linux 上运行的跨平台应用程序。

主进程
每个 Electron 应用都有一个单一的主进程,作为应用程序的入口。
主进程在 Nodejs 环境中运行,具有 require 模块和使用所有 Nodejs API 的能力。

渲染进程
主进程可以通过 BrowserWindow 创建窗口,生成一个单独的渲染器进程。
渲染器负责渲染网页内容。运行于渲染器进程中的代码遵循网页标准。
预加载脚本
主进程可以与操作系统交互,渲染进程只能渲染网页。当功能需要操作系统支持,渲染进程如何将需求传递给主进程,主进程如何将结果传递给渲染进程?
Electron 设计了一系列的 IPC 功能,方便主进程和渲染进程通信。渲染进程的通信通常在预加载脚本中进行。预加载脚本包含了在网页内容开始加载前就执行于渲染器进程的代码。这些脚本运行与渲染器进程中,却能访问 Nodejs API。为了安全考虑,它的 API 是有限的,主要是发起 IPC 请求或监听,将自定义的 API 和变量传递给渲染进程使用。
预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。
const { BrowserWindow } = require('electron')
// ...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/preload.js'
}
})
// ...
关于预加载脚本,后面有更详细的案例。
Electron 应用安全
解包 asar 文件
Electron 应用会将程序代码打包成 .asar
文件,该文件包含了程序的部分代码。
以 Follow(MacOS)为例,asar 文件路径为/Applications/Follow.app/Contents/Resources/app.asar
npm install -g @electron/asar
asar extract app.asar ./
软件供应链安全
Electron 应用使用的依赖包在 package.json 中
"dependencies": {
"dayjs": "^1.11.13",
"electron-log": "^5.2.3",
"electron-updater": "^6.1.8",
"i18next": "^23.16.4",
"i18next-electron-fs-backend": "^3.0.2",
"i18next-fs-backend": "^2.3.2",
"knex": "^3.1.0",
"lodash": "^4.17.21",
"node-machine-id": "^1.1.12",
"plist": "^3.1.0",
"zustand": "^5.0.1"
},
npm 支持对目录下的 package.json 以及 package-lock.json 自动分析,排查组件漏洞。
npm i --package-lock-only // 创建 package-lock.json
npm audit
Electron 版本
旧版本的 Electron 和 Chromium 可能存在直接可以利用的漏洞。
从解包出来的文件中可以初步判断 electron 版本。
➜ magic-asar grep -rn "electron\":"
./node_modules/electron-updater/package.json:33: "electron": "^31.2.1"
./node_modules/electron-log/package.json:29: "electron": "*",
./node_modules/electron-log/package.json:37: "vite-plugin-electron": "^0.15.5",
➜ magic-asar
远程调试
➜ ~ cd /Applications/Magic.app/Contents/MacOS
➜ MacOS ./Magic --inspect="0.0.0.0:7777" --watch

通过终端命令可以获取准确的 Electron 版本

对比版本发布明细
https://releases.electronjs.org/release/compare/v32.0.0/v32.2.5
与最新的稳定版本进行比较,查看旧版本存在的问题。
重要配置
通过 package.json 的 main 字段获取程序入口文件,这里是 dist-electron/main/index.js
可以确认一些重要配置,以便后续的漏洞利用。

nodeIntegration
从 Electron 5 版本开始默认设置为 false
这个属性决定渲染器进程是否具备执行 Nodejs 的能力。如果该属性设置为 true,渲染进程出现 XSS 漏洞,能够执行 Javascript 代码,就会导致 RCE 漏洞。
实际测试中渲染进程想要调用 require('child_process')
还需要 contextIsolation: false
和 sandbox 不显式设置为 true
contextIsolation
从 Electrion 12 版本开始默认设置为 true
上下文隔离主要是确保预加载脚本与网页(渲染进程)之间的对象隔离。如果在预加载脚本中设置 window.hello = 'ware'
并且启动了上下文隔离,当网页尝试访问 window.hello 对象时将返回 undefined。如果关闭该属性,渲染进程一旦出现 XSS 漏洞,能够执行覆盖 Javascript 代码,配合预加载脚本及主进程定义好的功能,可能导致 RCE 漏洞。
contextIsolation 本身隔离的效果不受 nodeIntegration、sandbox 的影响,渲染进程获取到预加载脚本的部分方法后,执行效果是受 sandbox 影响的,例如 Electron 6.0 以后,开启 sandbox 即使 Preload 将 require 绑定在了 window 对象中,渲染进程获取到 require 也无法加载 child_process ,当然,Preload 也加载不了。
漏洞案例
https://mksben.l0.cm/2020/10/discord-desktop-rce.html
预加载脚本内部重新封装了 require,禁止使用 child_process 和 shell。之后暴露给渲染进程。在关闭 contextIsolation 的情况下可以绕过限制,执行系统命令。
main.js
const { app, BrowserWindow } = require('electron')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
sandbox: false,
contextIsolation: false,
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
preload.js
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})
window.diyRequire = (module_name) => {
const forbidden_module = ["child_process", "shell"]
if (forbidden_module.indexOf(module_name) !== -1) {
console.log('not allow')
} else {
return require(module_name)
}
}
防御机制就在 if (forbidden_module.indexOf(module_name) !== -1)
中,由于 contextIsolation 关闭,渲染进程和预加载脚本共用一个上下文,通过原型链污染即可绕过限制,实现 RCE 。

sandbox
sandbox 是一项 Chromium 功能,它使用操作系统来显著地限制渲染器进程可以访问的内容,在 Electron 中,限制的方面还要包括 Node.js
能力
从 Electrion 20 版本开始,默认开启 sandbox,程序会被沙盒保护。但是当 nodeIntegration 、nodeIntegrationInSubFrames、nodeIntegrationInWorker 被设置为 true 时,sandbox 对于 Node.js 的保护效果会失效,除非显式设置 sandbox: true
。
webSecurity
webSecurity 控制是否开启同源策略,默认开启。
关闭 webSecurity 可能导致加载其他域的 Javascript 代码。
fuse
https://www.electronjs.org/zh/docs/latest/tutorial/fuses
Electron 是为了所有桌面开发者准备的,附带了比较全的功能。但并不是所有开发者都需要这些功能。因此 Electron 提供了关闭部分功能的 fuse。
这里只记录感觉有用的 fuse。
runAsNode
是否遵循 ELECTRON_RUN_AS_NODE环境变量。
ELECTRON_RUN_AS_NODE 的含义是把程序当成普通的 Node.js 进程启动。如果是普通的 Node.js,那么应该可以给程序传递很多启动参数。

nodeCliInspect
默认值:Enabled
是否遵循 --inspect --inspect-brk
,实际上该 fuse 决定是否允许远程调试。
grantFileProtocolExtraPrivileges
默认值:Enabled
这个 fuse 是关于 file://
协议的,在 Electron
中 file://
协议比 web 浏览器中的 file://
协议具备更强大的功能,包括但不限于:
file://
协议加载的页面可以通过fetch
加载其他file://
协议的资源file://
协议加载的页面能够使用service workers
file://
协议加载的页面能够访问子frames
file://
无视沙盒限制
官方推荐,加载本地文件尽可能使用自定义协议,而不是开启这个 fuse ,对于旧版本 Electron
,这是核心功能,所以默认开启。
预加载脚本
未开启 contextIsolation 及 sandbox
见 contextIsolation 部分
不安全的实现
开启安全配置后,预加载脚本自己很难造成大问题,除非自身代码存在漏洞,用户又可以利用。
案例:渲染进程可以读取 docs 目录下的文件,文件名由调用者提供,预加载脚本与主进程通信,读取并返回内容。
main.js
ipcMain.handle() 在主进程中设置一个处理程序,它会响应来自渲染进程(renderer.js)的 invoke 请求。这里设置了一个名为 'readFile' 的 IPC 通道,接收文件路径并返回文件内容。
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs');
const path = require('path')
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
win.loadFile('./index.html');
}
app.whenReady().then(() => {
ipcMain.handle('readFile', async (event, filePath) => {
try {
filePath = path.join(__dirname, filePath)
const data = await fs.promises.readFile(filePath, 'utf-8');
return data;
} catch (err) {
console.error('Error reading file:', err);
return null;
}
});
createWindow();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
preload.js
preload.js
用于在渲染进程的环境中注入 Node.js API,确保在渲染进程中有权限访问主进程的功能。通过 contextBridge
来暴露安全的 API,使渲染进程可以调用主进程的功能,但不暴露 Node.js 的底层 API,增加了安全性。
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('customAPI', {
readFile: async (fileName) => {
try {
const data = await ipcRenderer.invoke('readFile', `docs/${fileName}`);
return data;
} catch (error) {
console.error('Error invoking "readFile":', error);
return null;
}
},
});
renderer.js
renderer.js
是 Electron 渲染进程中的 JavaScript 文件,负责应用界面的呈现与交互逻辑,且通过 IPC 与主进程进行通信。
const fileNameInput = document.getElementById('fileNameInput');
const readFileButton = document.getElementById('readFileButton');
const fileContent = document.getElementById('fileContent');
readFileButton.addEventListener('click', async () => {
const fileName = fileNameInput.value;
const data = await window.customAPI.readFile(fileName)
fileContent.textContent = data || 'No content'
});
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Electron Path Traversal Vulnerability Demo</title>
</head>
<body>
<input type="text" id="fileNameInput" placeholder="Enter file name">
<button id="readFileButton">Read File</button>
<pre id="fileContent"></pre>
<script src="./renderer.js"></script>
</body>
</html>
这里没有检查文件名,导致任意文件读取漏洞。
接口过度暴露
在上面的例子中,我们通过预加载脚本使用 contextBridge 将一个文件读取 API 暴露给渲染进程。
需要暴露的功能多了,开发者可能就会偷懒:如果开发者直接将 ipcRenderer 或 ipcRenderer.invoke 这种 API 或非必要函数直接暴露给渲染进程,可能会导致渲染进程任意发起 IPC 通信,获取敏感信息。
案例:程序有很多与操作系统命令执行相关的功能,于是开发者写了一个主进程接受参数并执行的通信。
main.js
const { app, BrowserWindow, ipcMain } = require('electron')
const fs = require('fs')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('exec-command', async (event, cmd) => {
return new Promise((resolve, reject) => {
require('child_process').exec(cmd, (error, stdout, stderr) => {
if (error) {
console.error(`Error executing command "${cmd}":`, error);
reject(error);
} else {
resolve(stdout.trim());
}
});
});
});
createWindow();
});
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Electron RCE Demo</title>
</head>
<body>
<pre id="cmdResultContent"></pre>
<script src="./renderer.js"></script>
</body>
</html>
renderer.js
const fileContent = document.getElementById('cmdResultContent');
window.electronApi.invoke('exec-command', 'pwd').then((result) => {
fileContent.textContent = result || 'No cmd exec result available.'
})
preload.js
将 ipcRenderer.invoke
直接暴露到渲染进程的全局对象 window
下,允许渲染进程通过 electronApi.invoke
调用主进程的 IPC 方法。
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronApi', {
invoke: ipcRenderer.invoke,
});
此时就算代码里只是执行 pwd,但可以通过渲染进程调用主进程的 IPC 方法,存在 XSS 漏洞时即可 RCE。

CSP
CSP(内容安全策略)是应对 XSS 和数据注入攻击的一层保护措施。建议任何载入到 Electron 的站点都要开启。CSP 属于一种白名单机制,能够有效防止外部 JavaScript 执行。
检查方法:找到窗口加载的 html 文件,查看是否设置了 CSP 策略。
<meta http-equiv="Content-Security-Policy" content="script-src 'self';"/>
自定义协议
寻找 protocol.handle,关注自定义协议的实现是否安全。
不安全的实现
image:// 存在任意文件读取漏洞,自定义协议的杀伤范围更大。即使开启 contextIsolation,渲染器进程仍然可以访问自定义协议。
// image 协议注册
protocol.handle("image", async (request) => {
try {
// 解析 URL,移除协议前缀
const url = decodeURIComponent(request.url.replace(/^image:\/\//, ""))
const filePath = path.normalize(url) // 标准化路径
// 检查文件是否存在
if (fs.existsSync(filePath)) {
return new Response(fs.readFileSync(filePath), {
headers: { "Content-Type": "image/png" },
})
}
攻击实践
用户只要点击恶意链接,在渲染进程执行 javascript 就会被盗取本地文件。
参考
- https://xz.aliyun.com/t/12822
- https://www.electronjs.org/docs/latest
- https://www.electronjs.org/docs/latest/tutorial/security
- https://mksben.l0.cm/2020/10/discord-desktop-rce.html
- https://github.com/Just-Hack-For-Fun/Electron-Security