文件路径遍历(Path Traversal),也常被称为目录穿越(Directory Traversal),是指攻击者通过构造特殊的文件路径(如 ../../etc/passwd),访问到应用程序预期目录之外的文件。这类漏洞在 Web 应用中臭名昭著,但在 Electron 桌面应用中同样致命,因为主进程通常拥有完整的文件系统读写权限,一旦被恶意利用,攻击者可以读取、覆盖甚至删除用户机器上的任意文件。
24.3.1 漏洞的典型场景
在 Electron 应用中,文件路径遍历最常出现在以下两种情况下:
1. 用户输入直接拼接文件路径
假设你的应用提供了一个“导出报告”功能,用户可以输入文件名,然后应用将文件保存到指定目录。粗心的代码可能是这样的:
// 主进程(不安全!)
ipcMain.on('export-report', (event, fileName) => {
const fs = require('fs');
const content = generateReport();
// 直接拼接用户提供的文件名
const filePath = `./reports/${fileName}`;
fs.writeFileSync(filePath, content);
});
攻击者将文件名设为 ../../../../../../Windows/System32/drivers/etc/hosts,最终写入的路径就会变成 ./reports/../../../../../../Windows/System32/drivers/etc/hosts,经过路径解析后实际指向系统关键文件,造成严重破坏。
2. IPC 消息中携带不可信的文件路径
渲染进程可能向主进程发送一个“打开文件”的请求,并附上文件路径。如果主进程不进行校验就直接使用这个路径去读写文件,那么渲染进程中的任何代码(包括受 XSS 攻击注入的脚本)都能诱使主进程去操作任意文件。
// 主进程(不安全!)
ipcMain.handle('read-file', async (event, filePath) => {
const fs = require('fs').promises;
// 直接使用渲染进程传来的路径
return await fs.readFile(filePath, 'utf-8');
});
此类问题在启用了 nodeIntegration 或通过不安全的 contextBridge 暴露了文件操作接口的应用中尤其危险。
24.3.2 真实的攻击示例
假设一个 Electron 应用提供一个笔记导出功能,用户可以选择导出的“文件夹名称”,应用会创建一个以该名称命名的文件夹并将笔记存入其中。攻击者输入文件夹名:
../../../.ssh
如果应用没有对输入做任何校验,最终会在当前工作目录(通常是应用的可执行文件所在目录或用户主目录)的上层创建 .ssh 文件夹,并可能在内部写入攻击者可控的内容,覆盖 SSH 密钥,从而达到远程控制用户机器的目的。
另一个常见的目标是覆盖应用自身的配置文件或更新脚本,劫持应用行为,这在具有自动更新功能的 Electron 应用中尤为致命。
24.3.3 防护措施
防止路径遍历漏洞的核心原则是:永远不要信任来自渲染进程(或任何外部源)的文件路径或文件名,始终进行验证和规范化。 具体的防护手段包括:
1. 使用 path 模块规范化路径并验证基础目录
Node.js 的 path.resolve 和 path.normalize 可以将包含 .. 的路径转换成绝对路径,去除任何回溯。然后通过检查规范化后的路径是否以预期的基目录开头,来杜绝穿越。
const path = require('path');
const fs = require('fs').promises;
// 安全的文件写入
ipcMain.handle('safe-export-report', async (event, fileName) => {
const baseDir = path.resolve('/safe/reports'); // 绝对路径基目录
const resolvedPath = path.resolve(baseDir, fileName);
// 确保最终路径在 baseDir 内
if (!resolvedPath.startsWith(baseDir + path.sep)) {
throw new Error('非法的文件路径!');
}
await fs.writeFile(resolvedPath, content);
});
这段代码将用户输入的 fileName 解析为绝对路径,然后检查该绝对路径是否仍以 /safe/reports/ 开头(注意结尾加 path.sep 以避免类似 /safe/reports-fake 的绕过)。如果不在范围内,直接拒绝请求。
2. 限制允许的字符集
对于文件名类的输入,可以定义白名单正则表达式,只允许字母、数字、下划线、连字符等安全字符,彻底排除 ..、/、\ 等敏感符号。
function isValidFileName(name) {
// 只允许字母、数字、下划线和点号,长度 1-255
return /^[a-zA-Z0-9_.-]{1,255}$/.test(name);
}
3. 避免直接暴露文件系统 API 给渲染进程
渲染进程不应直接拥有 fs 等模块的访问权限。即便使用 contextBridge,也只应暴露封装好的、经过严格参数校验的高级接口。例如,渲染进程只能调用 exportReport('reportName'),而具体的路径拼接和安全检查在主进程或 Preload 中进行。
4. 警惕解压缩和文件导入功能
如果你的应用支持解压文件(如导入模板包、主题),必须检查压缩包内每个条目的路径,防止 ZIP slip 漏洞。处理时可以先获取条目规范化后的目标路径,确认它在目标解压目录之下才写入。
5. 保持最小权限
即使主进程拥有完整权限,也应该在代码中主动把可操作的文件范围限制在必要的目录下(如应用的用户数据目录 app.getPath('userData')),避免对整个文件系统随意操作。这样即便存在漏洞,攻击面也会被极大地缩小。
24.3.4 结合 Electron 安全最佳实践
路径遍历的防御不应孤立存在。配合前文所述的安全上下文隔离、禁用 nodeIntegration、启用 sandbox 等基础安全设置,能够有效降低渲染进程被攻陷后所能造成的危害。在所有 IPC 通信中,将“验证输入、拒绝不合法请求”作为固定思维,是构建健壮 Electron 应用的关键步骤。
安全的文件操作本质上是不可信数据与文件系统之间的防火墙。理解这一漏洞的原理,并在每一个文件读写入口处都落实严格的校验,就能让你的 Electron 应用远离这类常见却高危的风险。