Electron应用中如何保护主进程和渲染进程的XML文件交互

3次阅读

不能直接用 fs.readFile 在渲染进程读 xml,因 electron 渲染进程默认禁用 node.js 模块,require(‘fs’) 不可用;强行启用会破坏沙箱安全,应通过 ipcRenderer.invoke 调用主进程经白名单校验和安全解析的 xml:read 接口

Electron应用中如何保护主进程和渲染进程的XML文件交互

为什么不能直接用 fs.readFile 在渲染进程读 XML

Electron 渲染进程默认运行在沙箱环境(即使没显式开启 contextIsolation: true),require('fs') 也根本不可用。强行注入 Node.js 模块或关闭 nodeIntegration 都会大幅削弱安全边界,属于高危操作。

常见错误现象包括:ReferenceError: require is not definedfs is not defined,或更隐蔽的——XML 文件被意外写入恶意内容后,主进程未经校验直接解析,触发 XXE 或路径遍历。

  • 渲染进程只能通过 ipcRenderer 发起受控请求
  • 主进程必须对所有传入路径做白名单校验,禁止用户输入直接拼接 fs 调用
  • XML 解析必须禁用外部实体(libxmljs 默认启用,fast-xml-parser 默认禁用)

如何用 ipcMain.handle 安全暴露 XML 读取能力

主进程应只暴露最小必要接口,且每个调用都需验证路径合法性与文件扩展名。不建议把整个 fs 封装成通用 API。

const { app, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs').promises; const xmlParser = require('fast-xml-parser');  // 白名单:只允许读取 resources/xml/ 下的 .xml 文件 const ALLOWED_XML_DIR = path.join(app.getAppPath(), 'resources', 'xml');  ipcMain.handle('xml:read', async (event, filename) => {   // 1. 拒绝路径遍历   if (filename.includes('..') || filename.startsWith('/')) {     throw new Error('Invalid filename');   }   const fullPath = path.join(ALLOWED_XML_DIR, filename);      // 2. 确保解析前路径仍在白名单内   const resolved = await fs.realpath(fullPath);   if (!resolved.startsWith(ALLOWED_XML_DIR)) {     throw new Error('access denied');   }      // 3. 检查扩展名   if (path.extname(fullPath) !== '.xml') {     throw new Error('Only .xml files allowed');   }    try {     const content = await fs.readFile(fullPath, 'utf8');     // 4. 使用安全解析器(不解析 DTD / 外部实体)     const isValid = xmlParser.validate(content);     if (isValid !== true) throw new Error('Invalid XML structure');          return xmlParser.parse(content, {       ignoreAttributes: false,       ignoreNameSpace: true,       allowBooleanAttributes: false,       parseNodeValue: true,       parseAttributeValue: true,       trimValues: true     });   } catch (err) {     throw new Error(`Failed to read/parse ${filename}: ${err.message}`);   } });

渲染进程中如何调用并处理返回结果

使用 ipcRenderer.invoke 替代 send+on,避免竞态和未处理 promise。注意:返回的是解析后的 JS 对象,不是原始字符串 —— 这意味着你无法再用 DOMParser 处理命名空间等高级特性,但换来的是安全性。

  • 不要在渲染进程里拼接用户输入作为 filename 参数,应从预定义列表中选择
  • 捕获 invoke 的 rejection,避免未处理异常导致界面卡死
  • 若需保留原始 XML 字符串(例如用于 diff 或重写),主进程应额外提供 xml:raw-read 接口,且返回前仍需校验和长度限制(如 ≤ 512KB)
// 渲染进程(preload.js 中已上下文隔离) const { ipcRenderer } = require('electron');  async function loadConfigXml() {   try {     // filename 是前端可控但受限的值,比如来自下拉菜单选项 ['app-config.xml', 'theme-default.xml']     const data = await ipcRenderer.invoke('xml:read', 'app-config.xml');     console.log('Parsed config:', data);     return data;   } catch (err) {     console.error('Failed to load XML:', err);     alert('配置加载失败,请检查应用完整性');   } }

写 XML 文件时更要谨慎:主进程不能信任任何渲染进程传来的结构

写操作比读更危险。即使你校验了路径,也不能直接把渲染进程传来的 JS 对象交给 xmlBuilder 序列化 —— 因为对象可能含循环引用、非法字符、超长文本或构造好的恶意实体。

  • 主进程应定义严格 schema(如用 zodajv 校验),只接受明确字段
  • 序列化必须使用无副作用的生成器(如 xmlbuilder2),禁用 allowDoctypeencoding 动态设置
  • 写入前先 fs.stat 检查目标目录是否存在且为目录,防止覆盖关键文件
  • 写入后建议用 fs.chmod 设为只读(仅限桌面端本地配置场景)

最常被忽略的一点:XML 文件若用于存储用户偏好,其路径往往硬编码在主进程中;一旦攻击者通过原型污染或 IPC 消息伪造绕过校验,就可能写入 ../package.json../../main.js —— 所以白名单路径 + realpath 校验不是可选项,是必选项。

text=ZqhQzanResources