Electron 安全第一章:为什么一个 XSS 可能变成 RCE?

117 阅读10分钟

如今开发 Agent 客户端的团队越来越多,大部分人没有系统化的学习安全相关知识,在开发客户端时会发生非常多并且严重的安全事故,在接下来的一段时间,我将系统的梳理 Electron 开发中涉及安全的知识点,用具体的案例来帮大家理解以及分析安全问题,让大家的产品都能做到攻不可破!

Electron 世界 · 第一章:认识 Electron 安全

Electron 让我们可以使用熟悉的 HTML、CSS、JavaScript 构建桌面应用。但与此同时,它也把 Web 世界的安全问题带到了操作系统这一层。

在 Electron 中,一个看似普通的 XSS,可能不再只是“网页弹了一个 alert”,而可能进一步演变成本地文件读取、敏感信息窃取,甚至远程命令执行(RCE)。

这也是为什么,Electron 应用的安全性值得我们单独拿出来认真讨论。


一、为什么 Electron 需要安全?

如果你是一名前端开发者,可能已经非常熟悉 XSS。

例如一个普通 Web 页面存在这样的代码:

<div id="content"></div>

<script>
  content.innerHTML = userInput
</script>

如果用户输入:

<script>
  alert(document.cookie)
</script>

那么攻击者就可能在页面中执行自己的 JavaScript。

在传统 Web 应用中,我们通常会认为:

XSS = 攻击者控制了网页中的 JavaScript。

但这个结论放到 Electron 中,就不一定成立了。

因为 Electron 并不是一个单纯的浏览器。

它实际上把几个能力组合到了一起:

image.png

这意味着 Electron 应用既拥有 Web 世界的能力,也拥有桌面应用的能力。

例如:

  • 访问文件系统
  • 创建本地文件
  • 启动子进程
  • 访问系统信息
  • 调用操作系统 API
  • 创建窗口
  • 访问本地数据库
  • 调用系统命令

这些能力本身并不是问题。

真正的问题是:这些能力最终有没有被不可信的内容间接控制。


二、Web 应用和 Electron 应用最大的区别

对于普通的 Web 应用,浏览器会给网页设置一个非常重要的安全边界:

网页不能随便操作用户的操作系统。

例如网页中的 JavaScript 一般不能直接执行:

rm -rf /

也不能直接:

require('fs')

读取用户电脑上的任意文件。

这是浏览器 Sandbox 等安全机制共同建立起来的隔离环境。


而 Electron 的架构则更加复杂:

image.png

因为它意味着:

Web 世界和操作系统之间,存在了一条可以被开发者主动建立的桥梁。

这条桥梁用得好,它是 Electron 强大的核心,用得不好,它也可能成为攻击者进入操作系统的通道。


三、Electron 安全的核心,其实是“信任边界”

理解 Electron 安全之前,我建议先记住一个概念:

Trust Boundary —— 信任边界。

什么是信任边界?

简单来说就是:哪些东西可以相信,哪些东西不能相信?

image.png

这里有一个非常重要的原则:

不要因为代码运行在自己的 Electron 应用中,就默认它是可信的。

尤其是:

  • 远程网页
  • 第三方 SDK
  • 用户输入
  • 外部数据

这些内容都应该按照“不可信输入”来处理。


四、案例一:一个 XSS 为什么可能变成严重的安全问题?

假设应用加载了一个远程页面,同时为了开发方便写下:

win.loadURL('https://example.com')

new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,   // Renderer 可以直接用 Node.js
    contextIsolation: false  // 网页 JS 与 Preload 不隔离
  }
})

此时页面只要存在一个 XSS,攻击者就能从 alert('xss') 起步,一路走到 require('fs') 读取敏感文件、child_process.exec() 执行任意命令:

image.png

这就是 Electron 安全中非常重要的一个概念:

能力越强,信任边界被突破后的影响就越大。


五、案例二:IPC 写得不好,同样可能成为攻击入口

很多开发者知道:

“不要在 Renderer 中直接开启 Node Integration。”

于是改成:

Renderer
   ↓
Preload
   ↓
IPC
   ↓
Main

看起来安全了。

但是,如果 IPC 设计得不好,依然可能出现问题。

例如:

ipcMain.handle('execute-command', (_, command) => {
  return exec(command)
})

然后 Preload:

contextBridge.exposeInMainWorld('api', {
  executeCommand: (command) => {
    return ipcRenderer.invoke('execute-command', command)
  }
})

Renderer:

window.api.executeCommand(userInput)

表面上看,我们已经没有直接把 Node.js 暴露给 Renderer。

但是实际上:

Renderer
   ↓
executeCommand()
   ↓
IPC
   ↓
Main Process
   ↓
exec()
   ↓
操作系统

Renderer 依然获得了:

任意执行系统命令的能力。

所以:

Preload + IPC 并不会自动让 Electron 变得安全。

真正重要的是:

你到底向 Renderer 暴露了什么能力?


六、这就是 Electron 安全最重要的原则:最小权限

安全领域有一个非常经典的原则:

Principle of Least Privilege —— 最小权限原则。

什么意思?

假设我们的页面只需要:

读取当前用户信息

那么我们应该暴露:

window.user.getCurrentUser()

而不是:

window.electron = {
  ipcRenderer,
  fs,
  shell,
  childProcess
}

前者:

Renderer
   ↓
getCurrentUser()
   ↓
IPC
   ↓
Main
   ↓
返回用户信息

后者相当于:

Renderer
   ↓
大量系统能力
   ↓
操作系统

安全边界完全不同。

所以 Electron 安全并不是:

“把几个配置项改成 true/false 就结束了。”

而是:

从架构层面控制每一层拥有的权限。


七、Electron 的几道重要安全防线

我们可以把 Electron 的安全体系简单理解成几道防线。

第一层:Renderer 隔离

Renderer 是最接近 Web 内容的一层。

因此我们应该尽可能限制它的系统能力。

核心配置包括:

webPreferences: {
  nodeIntegration: false,
  contextIsolation: true,
  sandbox: true
}

它们分别解决不同的问题。

Node Integration

控制 Renderer 是否可以直接使用 Node.js。

通常情况下:

nodeIntegration: false

应该是默认选择。


Context Isolation

让网页 JavaScript 和 Preload 所处的 JavaScript 上下文隔离。

简单理解:

网页 JS
   │
   │ 不能直接污染
   ↓
Preload
   │
   ↓
Electron API

这可以降低网页内容直接影响 Electron 特权代码的风险。


Sandbox

进一步限制 Renderer 进程的系统能力。

可以把它理解成:

即使 Renderer 被攻击,也尽量把攻击者关在一个受限制的环境里。

所以:

Node Integration
Context Isolation
Sandbox

是理解 Electron 安全绕不开的三个概念。


八、第二层:Preload

如果 Renderer 不应该直接拥有 Node.js 能力,那么:

Renderer 到底怎么访问 Electron 的能力?

这就是 Preload 存在的意义。

例如:

contextBridge.exposeInMainWorld('userAPI', {
  getCurrentUser: () => {
    return ipcRenderer.invoke('user:getCurrentUser')
  }
})

Renderer:

const user = await window.userAPI.getCurrentUser()

这里最重要的不是 contextBridge 这个 API 本身。

而是:

Preload 应该成为一个“受控的能力出口”。

它应该暴露:

需要什么 → 暴露什么

而不是:

有什么 → 全部暴露

九、第三层:IPC

IPC 是 Renderer 和 Main Process 之间的桥梁。

但桥梁本身也需要安全检查。

例如:

ipcMain.handle('file:read', async (_, filePath) => {
  // ...
})

这里的 filePath 来自哪里?

答案是:

Renderer。

那么它就属于:

不可信输入。

因此不能直接:

fs.readFile(filePath)

而应该进行:

参数校验
   ↓
路径规范化
   ↓
权限检查
   ↓
目录限制
   ↓
执行操作

这就是 IPC 安全的核心:

IPC 不是信任边界的终点,而是需要重新建立信任的地方。


十、第四层:远程内容

Electron 最大的特点之一就是:

可以加载 Web 页面。

例如:

win.loadURL('https://example.com')

但是一旦加载远程内容,就需要开始考虑:

这个网站是谁?
        ↓
这个 URL 是否可信?
        ↓
是否允许跳转?
        ↓
是否允许打开新窗口?
        ↓
是否允许加载 WebView?
        ↓
页面是否可能被 XSS?
        ↓
页面是否能接触 Electron API?

因此,Electron 安全永远绕不开:

  • URL 白名单
  • Navigation 控制
  • Redirect 控制
  • CSP
  • XSS 防护
  • WebView 安全
  • 新窗口控制

十一、第五层:本地系统能力

Electron 最终是一个桌面应用。

因此我们还需要关注:

文件系统
   │
   ├── 任意文件读取
   ├── 任意文件写入
   └── Path Traversal

命令执行
   │
   ├── child_process
   ├── exec
   └── Command Injection

系统能力
   │
   ├── shell.openExternal
   ├── 自定义协议
   └── 系统信息

例如:

shell.openExternal(userInput)

看起来只是“打开一个链接”。

但是如果 userInput 完全由用户控制,就需要考虑:

这个 URL 到底是什么?

所以:

所有能够触达操作系统的 API,都应该被视为高权限能力。


十二、第六层:依赖与供应链

Electron 应用并不是只有我们自己写的代码。

一个真实项目可能包含:

Electron
├── Chromium
├── Node.js
├── V8
├── npm dependencies
├── Native Modules
├── 第三方 SDK
└── 自己的业务代码

因此安全问题也可能来自:

我们依赖的第三方代码。

例如一个 npm 包存在漏洞,那么我们的 Electron 应用可能同样受到影响。

所以 Electron 安全还需要关注:

  • Electron 版本
  • Chromium 安全更新
  • Node.js 安全更新
  • npm 依赖
  • Native Module
  • Lockfile
  • 依赖漏洞扫描
  • 软件供应链

这也是为什么:

Electron 安全不是一次性的工作,而是整个应用生命周期中的持续工作。


十三、Electron 安全到底应该关注哪些方向?

到这里,我们可以把 Electron 安全总结成下面这张图:

image.png

如果进一步总结,可以归纳成 6 个核心方向:

① Renderer 安全

控制网页到底拥有多少能力。

② Preload & IPC 安全

控制 Renderer 能够调用什么能力。

③ Web 内容安全

控制远程网页、XSS、URL、WebView 等风险。

④ 系统能力安全

控制文件、命令、协议、Shell 等高权限能力。

⑤ 数据与供应链安全

保护敏感数据,同时控制第三方依赖和运行时版本。

⑥ 安全审计与持续防护

从开发到发布,再到线上运行,持续发现和处理安全问题。


十四、一个非常重要的认知:Electron 安全不是“配置安全”

很多初学者会认为:

nodeIntegration: false
contextIsolation: true
sandbox: true

配置完成之后:

“我的 Electron 应用安全了。”

其实远远不够。

因为安全从来不是三个配置项的问题。

真正的安全模型应该是:

image.png

真正的目标是:

即使 Renderer 被攻击,也不能轻易突破下一层安全边界。

这就是所谓的:

Defense in Depth —— 纵深防御。


十五、我们应该如何看待 Electron 安全?

如果只把 Electron 当成一个:

“可以把 Web 页面打包成 exe / dmg 的工具。”

那么安全问题确实很容易被忽略。

但如果从另外一个角度看:

Electron 实际上是一个把 Web 技术、浏览器引擎、Node.js 和操作系统能力连接起来的平台。

那么它的安全问题就非常容易理解了。

Electron 安全的本质,就是不断建立和保护这些安全边界。


十六、写在最后

如果你正在开发 Electron 应用,我建议从今天开始,不要再简单地问:

“这个 API 能不能用?”

而应该多问一句:

“这个 API 应该让谁使用?”

也不要只问:

“这个页面是不是我们自己的?”

而应该问:

“如果这个页面今天被攻破了,它还能做什么?”

更不要认为:

“我们已经开启了 contextIsolation,所以安全了。”

而应该继续思考:

“Renderer → Preload → IPC → Main → OS,这条链路上的每一个环节,权限是否都是最小的?”

这才是 Electron 安全真正需要解决的问题。


下一章

在下一章,我们将正式进入 Electron 安全最核心的几个概念:

Sandbox、Context Isolation、Node Integration。

我们不会简单介绍三个配置项,而是从 Electron 的进程模型开始,真正理解:

Renderer 为什么不能拥有 Node.js?

Context Isolation 到底隔离了什么?

Sandbox 又解决了什么问题?

以及最重要的问题:

如果 Renderer 已经被攻击,我们还能不能让它停留在 Renderer?

这也是整个 Electron 安全体系的第一道防线。