人人都会AI编程

25.2 进程通信安全:参数校验、白名单校验、拒绝执行动态代码

更新时间:2026-07-11

Electron 的进程间通信(IPC)是渲染进程与主进程之间唯一的管道。一旦这条管道被恶意利用,攻击者就可能从界面层侵入系统层,执行任意文件操作甚至命令。因此,IPC 的安全性不能建立在“渲染进程是可信的”这种假设之上,而必须对每一条消息进行严格的验证和过滤。

本节从三个角度展开:参数校验、白名单校验、以及拒绝在主进程执行动态代码。这三个措施组合起来,能有效阻断绝大多数基于 IPC 的攻击路径。

25.2.1 参数校验:永远不要信任渲染进程的数据

渲染进程随时可能受到 XSS 攻击或第三方脚本的污染,因此主进程接收到任何消息时,第一件事就是校验参数的类型、格式和范围

原则

  • 对入参做类型检查,使用 typeofArray.isArrayinstanceof 等。
  • 对路径参数做严格限制,防止目录穿越(../、绝对路径等)。
  • 对命令参数使用白名单或正则匹配,避免注入。
  • 尽量使用 JSON Schema 或运行时校验库(如 zodjoi)统一管理。

真实案例:假设你有一个 IPC 通道允许渲染进程请求读取某个文件的内容。

❌ 不安全的实现:

// 主进程
ipcMain.handle('read-file', async (event, filePath) => {
  const content = await fs.readFile(filePath, 'utf-8');
  return content;
});

攻击者如果控制渲染进程,可以传入 /etc/passwd~/.ssh/id_rsa,直接窃取敏感文件。

✅ 安全的实现:

const path = require('path');
const { app } = require('electron');

// 只允许读取应用用户数据目录下的文件
const allowedDir = app.getPath('userData');

ipcMain.handle('read-file', async (event, filePath) => {
  // 1. 类型校验
  if (typeof filePath !== 'string' || filePath.length === 0) {
    throw new Error('Invalid file path');
  }
  // 2. 规范化路径并解析符号链接,然后检查是否在允许目录内
  const resolvedPath = path.resolve(allowedDir, filePath);
  if (!resolvedPath.startsWith(allowedDir)) {
    throw new Error('Path traversal detected');
  }
  // 3. 可选:进一步检查文件是否存在以及是否为普通文件
  const stat = await fs.stat(resolvedPath);
  if (!stat.isFile()) {
    throw new Error('Not a regular file');
  }
  return await fs.readFile(resolvedPath, 'utf-8');
});

这种“先解析再校验边界”的模式,是文件操作类 IPC 必须遵循的守则。

25.2.2 白名单校验:按功能精确放行 IPC 通道

Electron 的 IPC 基于字符串通道名,如果渲染进程可以随意调用任何通道,一旦某个通道存在漏洞,整个主进程就暴露了。白名单校验是指在主进程侧明确列出允许接收的通道名,并对每个通道单独绑定处理器,忽略未定义的通道。

做法

  • 使用一个集中的路由器对象管理所有 IPC 处理器。
  • 拒绝一切未知通道名的请求。
  • 结合 preload 暴露有限的方法,形成“双重白名单”。

例如,在 preload 中只暴露特定方法:

// preload.js
contextBridge.exposeInMainWorld('api', {
  readFile: (relativePath) => ipcRenderer.invoke('read-file', relativePath),
  // 不再暴露通用的 send 或 invoke,避免渲染进程随意构造通道
});

在主进程中,可以这样组织:

// main.js
const ipcMethods = {
  'read-file': async (event, filePath) => { /* 校验后读取 */ },
  'save-file': async (event, data) => { /* 校验后保存 */ },
};

ipcMain.handle('*', (event, channel, ...args) => {
  // 这并非有效语法,仅为示意;实际需要显式注册每个通道
});

// 实际实现:逐个注册
if (ipcMethods['read-file']) {
  ipcMain.handle('read-file', wrapHandler(ipcMethods['read-file']));
} else {
  // 通道不存在就记录警告,不做任何事
}

更严格的方案是使用一个注册表,并将未知通道调用视为攻击行为,直接上报并阻断,防止暴力探测。

25.2.3 拒绝在主进程执行动态代码

Electron 的 Node.js 环境允许使用 eval()new Function()child_process.exec() 等方式执行任意代码。如果这些能力以任何形式暴露给渲染进程,攻击者就能绕过所有参数校验,直接执行系统命令。

威胁模型

  • 渲染进程通过 IPC 传递给主进程一段需要“解析”的字符串,主进程用 JSON.parseeval 处理。
  • 主进程根据渲染进程的参数动态拼接命令字符串,再调用 exec

解决办法

  1. 永远不要使用 evalnew Function 处理来自渲染进程的数据,即便是间接方式也不行。
  2. 对于命令行调用,永远不要拼接字符串,而是使用 child_process.spawn 并传递参数数组,从根本上避免命令注入。
  3. 限制 child_process.exec 的使用,必须使用时,命令本身硬编码,只允许参数经过严格白名单过滤。
  4. webPreferences 中禁用 nodeIntegration,启用 contextIsolation,并考虑使用 sandbox 进一步限制渲染进程。

真实的反面案例
假设你提供给用户一个“自定义计算”功能,渲染进程发送公式,主进程计算:

ipcMain.handle('calc', (event, expression) => {
  // 危险!攻击者可传入 `require('fs').readdirSync('/')` 等
  return eval(expression);
});

这等同于开门揖盗。正确做法是使用安全的表达式解析器(如 math.jsnew Function 沙箱化),并严格限制可用的全局变量和模块。

针对 child_process 的安全策略
要用 spawn 代替 exec,因为 spawn 不接受 shell 命令字符串,参数与命令分离,天然防注入:

// 不安全
exec(`ls -la ${userInput}`); // 注入风险

// 安全
spawn('ls', ['-la', userInput]); // userInput 被当作一个参数,不会解析为多条命令

如果确实需要调用复杂命令,请将允许的命令和参数白名单化,并在运行时逐项匹配。

25.2.4 综合实践:一个安全的 IPC 架构模板

很多实际项目会将上述措施整合成一个健壮的 IPC 骨架:

  1. 定义通道与参数契约(TypeScript 中可以定义接口)。
  2. 在 preload 中暴露精确的函数,每个函数只调用一个通道。
  3. 主进程注册处理器,每个处理器开头调用统一的校验函数,该函数根据通道名从配置中读取参数约束模型(如 Zod schema)进行验证。
  4. 校验失败直接抛错并记录日志,不要返回详细信息给渲染进程,避免信息泄露。
  5. 彻底杜绝 eval 等动态执行,并审计所有 child_process 调用。

这样的分层防御,即便渲染进程被攻破,攻击者能做的也只是调用那些预先定义好的、有严格边界限制的功能,而无法肆意操控用户系统。

进程通信安全不是一次性的配置,而是贯穿整个 Electron 应用生命周期的持续实践。当你新增任何一个 IPC 接口时,都应当像审查 API 接口一样,追问:“如果调用方是恶意的,我能想到的最坏情况是什么?” 然后用本节的三板斧把那个漏洞堵上。