Electron 安全入门:为什么一个 XSS 可能变成 RCE?
Electron 世界 · 第一章:认识 Electron 安全
Electron 让我们可以使用熟悉的 HTML、CSS、JavaScript 构建桌面应用。但与此同时,它也把 Web 世界的安全问题带到了操作系统这一层。
在 Electron 中,一个看似普通的 XSS,可能不再只是"网页弹了一个 alert",而可能进一步演变成本地文件读取、敏感信息窃取,甚至远程命令执行(RCE)。
这也是为什么,Electron 应用的安全性值得我们单独拿出来认真讨论。
一、先思考一个问题:为什么 Electron 需要安全?
如果你是一名前端开发者,可能已经非常熟悉 XSS。
例如一个普通 Web 页面存在这样的代码:
xml
<div id="content"></div>
<script>
content.innerHTML = userInput
</script>
如果用户输入:
xml
<script>
alert(document.cookie)
</script>
那么攻击者就可能在页面中执行自己的 JavaScript。
在传统 Web 应用中,我们通常会认为:
XSS = 攻击者控制了网页中的 JavaScript。
但这个结论放到 Electron 中,就不一定成立了。
因为 Electron 并不是一个单纯的浏览器。
它实际上把几个能力组合到了一起:
┌───────────────────────────┐
│ Electron │
│ │
│ Chromium + Node.js + V8 │
│ │
└───────────────────────────┘
这意味着 Electron 应用既拥有 Web 世界的能力,也拥有桌面应用的能力。
例如:
- 访问文件系统
- 创建本地文件
- 启动子进程
- 访问系统信息
- 调用操作系统 API
- 创建窗口
- 访问本地数据库
- 调用系统命令
这些能力本身并不是问题。
真正的问题是:这些能力最终有没有被不可信的内容间接控制。
二、Web 应用和 Electron 应用最大的区别
我们先来看普通 Web 应用。
用户
↓
浏览器
↓
网页
↓
JavaScript
浏览器会给网页设置一个非常重要的安全边界:
网页不能随便操作用户的操作系统。
例如网页中的 JavaScript 一般不能直接执行:
bash
rm -rf /
也不能直接:
javascript
require('fs')
读取用户电脑上的任意文件。
这是浏览器 Sandbox 等安全机制共同建立起来的隔离环境。
而 Electron 的架构则更加复杂:
css
┌─────────────────────┐
│ Web 页面 │
│ HTML / JS / CSS │
└──────────┬──────────┘
↓
Renderer
↓
Preload
↓
IPC
↓
Main Process
↓
Node.js
↓
操作系统
这条链路非常重要。
因为它意味着:
Web 世界和操作系统之间,存在了一条可以被开发者主动建立的桥梁。
这条桥梁用得好,它是 Electron 强大的核心。
用得不好,它也可能成为攻击者进入操作系统的通道。
三、Electron 安全的核心,其实是"信任边界"
理解 Electron 安全之前,我建议先记住一个概念:
Trust Boundary ------ 信任边界。
什么是信任边界?
简单来说就是:
哪些东西可以相信,哪些东西不能相信?
例如:
arduino
可信
├── Electron Main Process
├── 自己打包的代码
├── 自己维护的 Preload
└── 自己控制的本地资源
不可信
├── 用户输入
├── 第三方网页
├── 远程 URL
├── WebView 加载的内容
├── 外部文件
└── IPC 传递过来的参数
这里有一个非常重要的原则:
不要因为代码运行在自己的 Electron 应用中,就默认它是可信的。
尤其是:
远程网页
第三方 SDK
用户输入
外部数据
这些内容都应该按照"不可信输入"来处理。
四、案例一:一个 XSS 为什么可能变成严重的安全问题?
假设我们正在开发一个 Electron 应用。
这个应用需要加载一个远程网站:
rust
win.loadURL('https://example.com')
同时,为了方便开发,我们错误地配置了:
yaml
new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
现在页面存在一个 XSS。
攻击者只需要让页面执行:
scss
alert('XSS')
看起来似乎没什么。
但是问题来了。
如果 Renderer 拥有 Node.js 能力,那么攻击者可能进一步尝试:
ini
const fs = require('fs')
const data = fs.readFileSync('/path/to/secret')
甚至进一步调用:
perl
const { exec } = require('child_process')
exec('some-command')
此时问题已经从:
网页 XSS
升级成:
XSS
↓
Node.js
↓
本地文件
↓
操作系统
这就是 Electron 安全中非常重要的一个概念:
能力越强,信任边界被突破后的影响就越大。
五、案例二:IPC 写得不好,同样可能成为攻击入口
很多开发者知道:
"不要在 Renderer 中直接开启 Node Integration。"
于是改成:
css
Renderer
↓
Preload
↓
IPC
↓
Main
看起来安全了。
但是,如果 IPC 设计得不好,依然可能出现问题。
例如:
bash
ipcMain.handle('execute-command', (_, command) => {
return exec(command)
})
然后 Preload:
bash
contextBridge.exposeInMainWorld('api', {
executeCommand: (command) => {
return ipcRenderer.invoke('execute-command', command)
}
})
Renderer:
javascript
window.api.executeCommand(userInput)
表面上看,我们已经没有直接把 Node.js 暴露给 Renderer。
但是实际上:
scss
Renderer
↓
executeCommand()
↓
IPC
↓
Main Process
↓
exec()
↓
操作系统
Renderer 依然获得了:
任意执行系统命令的能力。
所以:
Preload + IPC 并不会自动让 Electron 变得安全。
真正重要的是:
你到底向 Renderer 暴露了什么能力?
六、这就是 Electron 安全最重要的原则:最小权限
安全领域有一个非常经典的原则:
Principle of Least Privilege ------ 最小权限原则。
什么意思?
假设我们的页面只需要:
读取当前用户信息
那么我们应该暴露:
javascript
window.user.getCurrentUser()
而不是:
ini
window.electron = {
ipcRenderer,
fs,
shell,
childProcess
}
前者:
scss
Renderer
↓
getCurrentUser()
↓
IPC
↓
Main
↓
返回用户信息
后者相当于:
Renderer
↓
大量系统能力
↓
操作系统
安全边界完全不同。
所以 Electron 安全并不是:
"把几个配置项改成 true/false 就结束了。"
而是:
从架构层面控制每一层拥有的权限。
七、Electron 的几道重要安全防线
我们可以把 Electron 的安全体系简单理解成几道防线。
第一层:Renderer 隔离
Renderer 是最接近 Web 内容的一层。
因此我们应该尽可能限制它的系统能力。
核心配置包括:
yaml
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
它们分别解决不同的问题。
Node Integration
控制 Renderer 是否可以直接使用 Node.js。
通常情况下:
vbnet
nodeIntegration: false
应该是默认选择。
Context Isolation
让网页 JavaScript 和 Preload 所处的 JavaScript 上下文隔离。
简单理解:
网页 JS
│
│ 不能直接污染
↓
Preload
│
↓
Electron API
这可以降低网页内容直接影响 Electron 特权代码的风险。
Sandbox
进一步限制 Renderer 进程的系统能力。
可以把它理解成:
即使 Renderer 被攻击,也尽量把攻击者关在一个受限制的环境里。
所以:
css
Node Integration
Context Isolation
Sandbox
是理解 Electron 安全绕不开的三个概念。
八、第二层:Preload
如果 Renderer 不应该直接拥有 Node.js 能力,那么:
Renderer 到底怎么访问 Electron 的能力?
这就是 Preload 存在的意义。
例如:
arduino
contextBridge.exposeInMainWorld('userAPI', {
getCurrentUser: () => {
return ipcRenderer.invoke('user:getCurrentUser')
}
})
Renderer:
javascript
const user = await window.userAPI.getCurrentUser()
这里最重要的不是 contextBridge 这个 API 本身。
而是:
Preload 应该成为一个"受控的能力出口"。
它应该暴露:
需要什么 → 暴露什么
而不是:
有什么 → 全部暴露
九、第三层:IPC
IPC 是 Renderer 和 Main Process 之间的桥梁。
但桥梁本身也需要安全检查。
例如:
dart
ipcMain.handle('file:read', async (_, filePath) => {
// ...
})
这里的 filePath 来自哪里?
答案是:
Renderer。
那么它就属于:
不可信输入。
因此不能直接:
scss
fs.readFile(filePath)
而应该进行:
参数校验
↓
路径规范化
↓
权限检查
↓
目录限制
↓
执行操作
这就是 IPC 安全的核心:
IPC 不是信任边界的终点,而是需要重新建立信任的地方。
十、第四层:远程内容
Electron 最大的特点之一就是:
可以加载 Web 页面。
例如:
rust
win.loadURL('https://example.com')
但是一旦加载远程内容,就需要开始考虑:
markdown
这个网站是谁?
↓
这个 URL 是否可信?
↓
是否允许跳转?
↓
是否允许打开新窗口?
↓
是否允许加载 WebView?
↓
页面是否可能被 XSS?
↓
页面是否能接触 Electron API?
因此,Electron 安全永远绕不开:
- URL 白名单
- Navigation 控制
- Redirect 控制
- CSP
- XSS 防护
- WebView 安全
- 新窗口控制
十一、第五层:本地系统能力
Electron 最终是一个桌面应用。
因此我们还需要关注:
bash
文件系统
│
├── 任意文件读取
├── 任意文件写入
└── Path Traversal
命令执行
│
├── child_process
├── exec
└── Command Injection
系统能力
│
├── shell.openExternal
├── 自定义协议
└── 系统信息
例如:
scss
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 安全总结成下面这张图:
css
Electron 安全
│
┌───────────────────┼───────────────────┐
│ │ │
Renderer IPC Web 内容
│ │ │
Sandbox 权限控制 XSS / CSP
Context Isolation 参数校验 URL 校验
Node Integration API 暴露 WebView
│ │ │
└──────────────┬────┴───────┬───────────┘
│ │
本地系统能力 数据与供应链
│ │
文件 / 命令 / URL 依赖 / Electron
│ │
└──────┬─────┘
│
安全审计
│
开发 → 构建 → 发布 → 运行
如果进一步总结,可以归纳成 6 个核心方向:
① Renderer 安全
控制网页到底拥有多少能力。
② Preload & IPC 安全
控制 Renderer 能够调用什么能力。
③ Web 内容安全
控制远程网页、XSS、URL、WebView 等风险。
④ 系统能力安全
控制文件、命令、协议、Shell 等高权限能力。
⑤ 数据与供应链安全
保护敏感数据,同时控制第三方依赖和运行时版本。
⑥ 安全审计与持续防护
从开发到发布,再到线上运行,持续发现和处理安全问题。
十四、一个非常重要的认知:Electron 安全不是"配置安全"
很多初学者会认为:
vbnet
nodeIntegration: false
contextIsolation: true
sandbox: true
配置完成之后:
"我的 Electron 应用安全了。"
其实远远不够。
因为安全从来不是三个配置项的问题。
真正的安全模型应该是:
css
不可信内容
│
▼
Renderer
│
┌──────┴──────┐
│ │
隔离/沙箱 权限限制
│ │
└──────┬──────┘
▼
Preload
│
最小化 API
│
▼
IPC
│
参数校验 + 权限校验
│
▼
Main
│
系统能力控制
│
▼
OS
真正的目标是:
即使 Renderer 被攻击,也不能轻易突破下一层安全边界。
这就是所谓的:
Defense in Depth ------ 纵深防御。
十五、我们应该如何看待 Electron 安全?
如果只把 Electron 当成一个:
"可以把 Web 页面打包成 exe / dmg 的工具。"
那么安全问题确实很容易被忽略。
但如果从另外一个角度看:
Electron 实际上是一个把 Web 技术、浏览器引擎、Node.js 和操作系统能力连接起来的平台。
那么它的安全问题就非常容易理解了。
css
Web 世界
│
│ 不可信内容
▼
┌─────────┐
│Renderer │
└────┬────┘
│
安全边界
│
┌────▼────┐
│ Preload │
└────┬────┘
│
安全边界
│
┌────▼────┐
│ IPC │
└────┬────┘
│
安全边界
│
┌────▼────┐
│ Main │
└────┬────┘
│
▼
操作系统
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 安全体系的第一道防线。