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

为什么不能直接用 fs.readFile 在渲染进程读 XML
Electron 渲染进程默认运行在沙箱环境(即使没显式开启 contextIsolation: true),require('fs') 也根本不可用。强行注入 Node.js 模块或关闭 nodeIntegration 都会大幅削弱安全边界,属于高危操作。
常见错误现象包括:ReferenceError: require is not defined、fs 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(如用
zod或ajv校验),只接受明确字段 - 序列化必须使用无副作用的生成器(如
xmlbuilder2),禁用allowDoctype和encoding动态设置 - 写入前先
fs.stat检查目标目录是否存在且为目录,防止覆盖关键文件 - 写入后建议用
fs.chmod设为只读(仅限桌面端本地配置场景)
最常被忽略的一点:XML 文件若用于存储用户偏好,其路径往往硬编码在主进程中;一旦攻击者通过原型污染或 IPC 消息伪造绕过校验,就可能写入 ../package.json 或 ../../main.js —— 所以白名单路径 + realpath 校验不是可选项,是必选项。










