拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Electron 安全攻防:从 XSS 到 RCE 的完整链路与加固方案

你手里的 Electron 应用可能比你想的更危险。Electron 是现在桌面应用开发最流行的方案之一VS Code、Slack、Discord、Notion 这些产品都跑在它上面但正因为 Electron 把 Chromium 和 Node.js 揉在一起安全模型和传统 Web 完全不同。一个在浏览器里可能只算“小问题”的 XSS放到 Electron 渲染进程里一旦配合不当的配置或暴露过多的 preload 接口很快就能变成 RCE远程代码执行。这篇文章我会从 Electron 的架构讲起分析 XSS 为什么能在桌面端“升级”再用一个本地实验 demo 完整走一遍从 XSS 到 RCE 的攻击链路接着讲怎么逆向复查一个已经打包好的 .asar 文件最后给出一套可以落地的加固方案。适合正在用 Electron 做产品的开发者、做企业安全自查的同学以及对桌面端漏洞原理感兴趣的白帽新手。1. 先搞懂 Electron 的“三重身份”Electron 应用里至少有三种执行环境很多人只盯着页面代码写业务根本没意识到这三个环境之间的边界才是安全问题的高发区。1.1 主进程、渲染进程、preload 到底各是什么Electron 应用启动之后会有一个主进程它是 Node.js 环境拥有完整的系统权限读写文件、启动子进程、调用系统 API、控制窗口生命周期。主进程通常负责创建 BrowserWindow、管理应用菜单、处理系统托盘、注册全局快捷键等。每个窗口对应一个渲染进程跑的是 Chromium 内核加载 HTML/CSS/JS。默认情况下渲染进程是“网页沙箱”没有 Node.js 能力和浏览器里的页面差别不大。但是 Electron 历史上的很多配置项可以打开权限最典型的就是nodeIntegration: true一旦打开渲染进程里的 JS 就能直接用require(child_process)之类的东西操作操作系统。preload 脚本夹在中间。它运行在渲染进程加载网页之前但拥有一个特殊的位置可以同时接触到一部分 Node.js API 和页面 DOM。Electron 官方推荐用 preload 配合contextBridge暴露一组受控接口给页面而不是直接把 Node 能力丢给渲染进程。简单类比主进程是公司机房管理员钥匙全在手里渲染进程是访客休息室访客能在里面自由活动但不能碰机房设备preload 是前台接待访客只能通过前台叫某些服务前台给不给是另一回事。安全事故大多数是前台太“好说话”或者机房管理员把钥匙落在了休息室。1.2 为什么浏览器里的 XSS 在 Electron 里会成为 RCE传统 Web 场景里 XSS 的危害一般停留在盗用 Cookie、伪造用户操作、钓鱼等因为浏览器沙箱把 JS 限制在了当前页面和同源策略范围内。Electron 不一样渲染进程紧挨着 Node.js 能力中间可能只隔着一层薄薄的配置。如果开发者图方便开启了nodeIntegration: true渲染进程里的一段恶意脚本就可以直接执行require(child_process).exec(calc.exe);在 Windows 上这就是一次弹出计算器的常见“概念验证”。攻击者当然不会只做这一步换成 PowerShell 下载脚本、写启动项、窃取本地敏感文件都只是同一段 Node 代码的问题。所以 Electron 应用里XSS 不再是“存储型还是反射型”的问题而是一旦触发就等于在用户电脑上执行任意命令。就算开发者没有开nodeIntegration只要 preload 脚本里用contextBridge暴露了“过于通用”的接口攻击者照样能借力打力。比如暴露出一个sendIpc(channel, data)方法渲染进程里的恶意代码就能绕过 Node 限制直接和主进程通信。如果主进程某个 IPC 监听器没有做来源校验还处理了 shell 命令那就绕了一圈又回到 RCE。这种“间接 RCE”恰恰是 Electron 安全里最隐蔽的部分代码审计的人看主进程嗯没有nodeIntegration觉得挺安全但没细看 preload 暴露了哪些通道看渲染进程又觉得反正没有 Node 权限XSS 就 XSS 吧。两个“安全”拼在一起实际是足够致命的漏洞组合。2. 攻击链路拆解XSS 是怎么一步步走到 RCE 的我习惯把一道 Electron 漏洞链条拆成四个环节XSS 入口、执行环境权限、特权接口暴露、主进程处理。任意一环做得足够好都能把 RCE 拦下来。反过来说只要四个环节里有两个环节“放水”基本就跑不掉了。2.1 入口渲染进程里的 XSS 如何出现Electron 的渲染进程本质还是网页所以传统 XSS 手法在这里通通适用。最常见的是这两类开发者用innerHTML、outerHTML、insertAdjacentHTML直接拼接用户输入没有做转义或过滤加载了第三方不可信脚本或者通过webview嵌入外部页面导致不受控代码在渲染进程里执行我在本地复现用的就是一个典型 DOM 型漏洞。假设页面从 URL 参数读取关键词并渲染到页面上代码类似这样div idresult/div script const params new URLSearchParams(location.search); document.getElementById(result).innerHTML 搜索结果 params.get(q); /script这个页面本身没有任何“数据库”也没接后端看起来人畜无害。但只要访问时带上?q恶意参数浏览器解析 HTML 时就会把参数里的标签当作 DOM 节点渲染出来脚本也能执行。传统的“修复”方式是过滤script标签但是像img srcx onerror...这类事件属性同样可以触发 JS所以靠黑名单过滤很难彻底。2.2 权限窗口不同配置下的放大效果我用同一个 XSS payload在三种不同配置下做对比结果差异巨大。webPreferences 配置XSS 后续能力nodeIntegration: true, contextIsolation: false页面脚本可直接require(child_process)一步 RCEnodeIntegration: false, contextIsolation: false页面脚本能访问到 preload 暴露在 window 上的接口如果接口通道太宽仍可 RCEnodeIntegration: false, contextIsolation: true, sandbox: true页面脚本无法直接接触 Node 和 preloadXSS 影响范围被压缩在页面内部第一组配置在老项目里非常常见尤其不少从 2016 年前后开始做 Electron 的小团队模板代码里往往直接抄早期官方示例。打开 DevTools 看一下全局对象甚至能看到require、process、Buffer这些 Node 标识符挂在 window 下这种应用基本就是“谁拿到 XSS 谁拿管理员权限”。第二组配置比第一组好一点但未必安全。如果 preload 里写了window.api { run: (cmd) ipcRenderer.send(exec, cmd) }页面脚本调用window.api.run(calc.exe)就能把命令发给主进程。所谓“contextIsolation 关闭”意味着 preload 和页面共享同一个 JavaScript 上下文页面脚本可以直接遍历并调用 preload 里的所有对象隔离名存实亡。第三组是目前官方推崇的安全基线能挡住绝大多数“借力型”攻击但前提是开发者不要把主进程里的敏感操作全部封装成无校验的 IPC 接口。2.3 特权接口preload 暴露了什么才是关键很多人以为关了 nodeIntegration 就万事大吉实际上 preload 才是真正需要重点审查的对象。Electron 官方推荐的姿势是const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(secureApi, { getSystemUptime: () ipcRenderer.invoke(get-system-uptime) });这样页面只能调用一个名字为getSystemUptime的方法方法背后固定走get-system-uptime通道主进程也只在这个通道上返回明确的字符串数据。攻击者就算有 XSS能做的事情也很有限。但实际项目里经常出现这些“危险暴露”暴露send/invoke的通用封装比如window.electron.send(任意通道, 任意参数)暴露直接读取本地文件的方法比如readFile(path)暴露执行命令的方法比如openTerminal(command)本意是给用户做个按钮结果成了万能后门暴露能返回主进程内部对象的 getter主进程对象一旦暴露渲染进程就能顺着原型链挖更多方法我在评估一个 Electron 项目安全性时一定会把 preload 文件从头到尾读一遍。如果它只暴露两三个细粒度业务方法风险很低如果暴露了invoke原始方法并允许调用方自定义 channel基本和高危漏洞无异。2.4 主进程最后一层防线是否接了“手雷”即使渲染进程能发 IPC 消息主进程如果对所有通道做了严格校验攻击也没法继续。需要重点检查的是主进程里的这些监听器ipcMain.on(open-external, (event, url) { shell.openExternal(url); }); ipcMain.on(save-file, (event, data) { fs.writeFileSync(/tmp/data, data); }); ipcMain.on(run-cmd, (event, cmd) { exec(cmd); });这三个都是我在真实代码里见过的“反面典型”。shell.openExternal如果没限制协议恶意 URL 里可能有各种协议处理程序在 Windows 上某些协议处理器甚至可以直接执行命令。save-file如果没校验路径配合 XSS 可以往任意位置写文件。run-cmd就不用说了通道本身就是在裸奔。加固的正确方式是通道名固定、参数白名单化、来源校验、返回结构约束。简单说主进程不要“来者不拒”。3. 本地实验从 XSS payload 到 RCE 的完整走查写安全分析不能空谈还是要亲手跑一遍才踏实。我建了一个最小实验项目代码全部在本地环境执行用最简单的方式演示整个链路。3.1 搭建一个“带病”的 Electron 环境项目结构如下electron-xss-demo/ ├── package.json ├── main.js └── renderer/ └── index.htmlmain.js故意模拟一个老项目const { app, BrowserWindow } require(electron); const path require(path); function createWindow() { const win new BrowserWindow({ width: 1280, height: 800, webPreferences: { nodeIntegration: true, contextIsolation: false, }, }); win.loadFile(path.join(__dirname, renderer, index.html)); } app.whenReady().then(createWindow);index.html里就是那个存在 XSS 的搜索展示页。这里我故意不设置任何 CSP也不对用户输入做转义好让后续测试可复现。3.2 用 URL 参数触发 XSS启动应用后用 URL 访问index.html?qimg srcx onerrorrequire(child_process).exec(calc.exe)因为nodeIntegration: true渲染进程里的全局require是标准 Node 模块加载器。当页面执行innerHTML赋值时img 标签加载失败就会触发onerror回调里执行require(child_process).exec(calc.exe)直接在用户电脑上启动计算器。这只是一个无害的本地验证。替换calc.exe为任何你想要的命令就是完整 RCE所以在真实系统上测试时务必只在隔离虚拟机里做不要拿工作电脑当靶场。3.3 模拟关闭 nodeIntegration 后的第二种路径把main.js改成webPreferences: { nodeIntegration: false, contextIsolation: false, preload: path.join(__dirname, preload.js), }preload.js里写一个“为了业务方便”的接口const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(bridge, { send: (channel, payload) ipcRenderer.send(channel, payload), });主进程里配合ipcMain.on(open-shell, (event, target) { const { exec } require(child_process); exec(target); });此时页面脚本没有require但window.bridge.send(open-shell, calc.exe)仍然是可用的。恶意 payload 只需要改为img srcx onerrorwindow.bridge.send(open-shell,calc.exe)这一串就能绕过 nodeIntegration 的限制走 IPC 通道让主进程执行命令。可见“关闭 nodeIntegration”只是一个必要条件不是充分条件。3.4 本地实验给出的安全结论这次实验给我最直观的感受是XSS 在 Electron 里的“危害指数”不取决于 XSS 本身而取决于它周围暴露了多少特权功能。开发者真正要管理的不是“绝对不能有 XSS”而是“即使有 XSS系统也应无权限可利用”。后者听上去更难实际上是可以通过统一规范实现的。4. 逆向已打包的 Electron 应用从 asar 里挖真相不少团队会把自己开发的 Electron 应用拿去做安全自查也会遇到需要对第三方应用做分析的情况。无论哪种目的先要解决一个技术问题已经打包好的应用源码怎么恢复4.1 解包 asar 文件的三种方式Electron 打包后的源码一般放在resources/app.asar。asar 本质是一种把多个文件拼在一起的归档格式并不是加密。解包方式很多我用过比较顺手的有三种方法命令/操作适用场景electron/asar命令行npx electron/asar extract app.asar ./app_src快速解包整个目录asar查看单个文件npx electron/asar list app.asar先看文件列表再按需提取全局替换归档工具解析.asar的二进制格式后手动恢复处理部分损坏文件或定制需求提取后就能看到熟悉的package.json、main入口、renderer 目录、node_modules 等结构。开发者必须清楚一件事Electron 应用并不是“编译型二进制”任何前端代码都能被完整还原。不要把密钥、接口签名、加密盐直接写死在渲染进程代码里否则等于把保险箱密码贴在保险箱门上。4.2 逆向 Electron 包的四个重点排查位置打开解包目录后我一般按这套顺序来做快速体检第一是package.json。看main字段指向谁看scripts里有没有调试后门比如启动时带--remote-debugging-port或--inspect。如果打包后的应用默认开启远程调试端口攻击者可以连接 9222 端口直接控制页面上下文这比找 XSS 还要轻松。第二是主进程入口文件。搜索ipcMain.on、ipcMain.handle、nodeIntegration、contextIsolation、sandbox、shell.openExternal、child_process这些关键字。把每个 IPC 通道和对应处理方法列出来建立一张“通道-参数-动作”映射表。第三是 preload 脚本。搜索contextBridge.exposeInMainWorld看暴露出来的接口名称和方法签名。如果接口里包含invoke、send、execute、open这类宽泛词就要重点确认有没有做 channel 白名单。第四是 renderer 目录。所有 JS 文件都还原成明文可以直接搜硬编码密钥、API endpoint、内网地址、注释里的账号密码。很多自以为“安全”的应用在逆向面前透明得像玻璃。4.3 逆向视角下的威胁模型我做过一个第三方 Electron 工具的分析主进程配置看起来挺安全nodeIntegration 是 falsecontextIsolation 是 truesandbox 开着。但 preload 暴露了一个接口允许页面传入一个完整路径去读取文件内容。攻击者只要在页面上找到一处 XSS就能读取用户本地的任一个已知路径文件比如C:\Users\xxx\.ssh\id_rsa。这种漏洞不会直接弹出计算器但危害比弹计算器严重得多。所以逆向审查时不要只看“标准安全字段”要把注意力放在“业务功能被滥用后会造成什么后果”上。File Read、Arbitrary Command、Unauthorized IPC 这三类问题在 Electron 应用里都属于高危。5. 加固清单把 XSS 变 RCE 的路一条条堵死如果让我给一套能直接落地的加固方案下面这些是必须做的。5.1 必须正确配置的 webPreferences创建BrowserWindow时建议所有窗口统一使用这份安全基线new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true, allowRunningInsecureContent: false, preload: path.join(__dirname, preload.js), } });webSecurity: true保证同源策略生效。allowRunningInsecureContent: false限制 HTTPS 页面加载 HTTP 资源。这两个字段容易被忽略但在 Electron 里同样重要。如果业务确实需要 openExternal 打开外链一定要限制协议。原则上只允许https:尽量不要允许http:绝对不要允许file:、smb:、custom-protocol:这类协议。5.2 preload 和 IPC 的暴露规范preload 里的接口要遵循“最小暴露”原则。每增加一个 exposed API都要等于多开一扇窗口。下面这个规范我建议直接写进团队 Code Review 的 check list禁止暴露ipcRenderer.send/ipcRenderer.invoke的通用封装每个方法名对应一个固定 IPC channel方法内部不允许动态拼接 channelIPC 方法的参数必须有限定比如字符串长度、枚举范围、路径前缀校验主进程监听 IPC 时校验event.senderFrame.url或event.sender.id确保请求来自白名单页面敏感操作写文件、执行命令、删除文件必须二次确认最好带上一次性 token一个相对稳妥的写法contextBridge.exposeInMainWorld(api, { openHelp: () ipcRenderer.invoke(help:open), saveReport: (reportData) ipcRenderer.invoke(report:save, reportData), });主进程这边ipcMain.handle(report:save, (event, reportData) { const allowedSender http://localhost:3000; if (event.senderFrame.url.startsWith(allowedSender) false) { throw new Error(untrusted sender); } // 这里再对 reportData 做结构校验 return { ok: true }; });5.3 渲染进程自身的 XSS 防御Electron 渲染进程依然适用 Web 防御手段。所有用户输入输出到 HTML 时优先用textContent不用innerHTML。确实需要富文本渲染的使用成熟库做白名单过滤同时设置严格的 CSP。CSP 示例Content-Security-Policy: default-src self; script-src self; style-src self unsafe-inline; img-src self data:;这句策略限制了脚本只能从当前应用自身加载第三方域名和eval都会被拦下。Electron 的loadURL和loadFile支持设置 HTTP 头或 meta 标签把 CSP 加上之后即使存在 DOM XSS注入的脚本也没法加载外部恶意代码让利用难度大幅上升。5.4 依赖和版本问题同样要盯Electron 是 Chromium Node.js 的合体两者都不断有 CVE 爆出。老版本的 Electron 可能带有知名沙箱逃逸漏洞即使应用代码写得再安全底层库有洞也一样出事。建议固定 Electron 主版本并跟进官方安全更新关注 Electron Releases 的公告新版本发布后评估是否需要升级清理 node_modules 中不需要的第三方依赖尤其避免引入带原生模块的不明库安装依赖前检查包是否正常警惕 npm 投毒类风险安全不是一次配置就完结而是要持续跟进。6. 常见问题速查排查 Electron 安全问题时的真实场景这部分整理我实际踩过的、以及做安全咨询时被频繁问到的典型问题。6.1 为什么我在渲染进程里明明写了require(electron)却报错原因大概率是nodeIntegration为false。新版 Electron 默认关闭 Node.js 能力渲染进程不再有require、process、Buffer这些全局对象。解决方案不是把nodeIntegration改回true而是把需要的能力放到 preload 里再用contextBridge暴露。6.2 contextIsolation 打开后为什么页面访问不到window.api一个常见误操作是页面在 preload 执行完之前就读取 API或者 preload 里使用contextBridge.exposeInMainWorld后页面却通过window.onload之前的同步脚本读取。另一个可能是 preload 脚本执行出错比如ipcRenderer没有正确导入。建议先用 DevTools 在页面控制台输入Object.keys(window)检查 API 是否真的存在再看 Console 里是否有 preload 报错。6.3 如何快速判断一个 Electron 应用是否开启了 nodeIntegration先在应用菜单里找到“切换开发者工具”或按 F12/CtrlShiftI 打开 DevTools在 Console 里输入typeof require。如果显示function说明 nodeIntegration 开着风险很高。如果显示undefined还要再看window.process是否存在部分版本下process可能仍然可访问。更稳妥的方式是解包 asar 后直接读主进程配置。6.4 我只想禁掉 XSS不想要 CSP 把内联脚本全拦了怎么办不少前端团队习惯用内联脚本设了严格 CSP 之后功能全挂就干脆不加。可行折衷是把业务脚本全部外链化并把内联事件处理器替换成addEventListener然后 CSP 用self。如果必须允许少量内联脚本可以用unsafe-inline配 nonce 机制但是要注意 nonce 不能被攻击者预测也不能出现在静态 HTML 属性里被 XSS 泄露。6.5 Electron 里能直接把网页渲染到webview标签里吗webview是一个特殊标签和 iframe 不同它拥有独立进程但同样存在安全风险。官方文档明确不建议使用webview很多安全通告里的 RCE 案例就是围绕它展开的。我个人的建议是能不用就不用如果要用必须严格设置partition、allowpopups、webpreferences等属性并确保外部内容不可信时不会获得特权 API。7. 最后再分享一个排查小技巧我最近复查一个 Electron 项目时发现表面上所有安全配置都合规但主进程里有一段ipcMain.on(call-http, (e, url) net.fetch(url))本来只想让渲染进程转发一个简单请求。攻击者通过 XSS 传一个 file:// URL 进来利用主进程的net.fetch读取本地文件再把内容回传到恶意服务器。这个问题纯粹靠字段配置发现不了必须在代码审查时把每个 IPC 通道当成“攻击面”来测。所以我给自己养成了一个习惯每次审查完 Electron 代码都会在本地跑一遍“最小攻击路径”复现。从能找到的最弱 XSS 入口出发一步步看它能摸到哪一层特权接口。只要一次实验里走到了“读文件”或“执行命令”我就直接给该项目标高风险。如果你也想做 Electron 安全自查最快的方法不是从头看文档而是先解包自己的应用假装自己是个攻击者翻代码。这套流程走完大部分问题会自己浮出来。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门