人人都会AI编程

25.3 本地文件与用户数据安全规范

更新时间:2026-07-11

在 Electron 应用中,本地文件读写和用户数据存储是最常见的需求,同时也是最容易引入安全隐患的环节。由于主进程拥有完整的 Node.js 能力,一个不严谨的文件操作就可能被恶意利用,导致用户数据泄露甚至系统损坏。本节提供一组可立即落地的安全规范,帮助你构建让用户真正放心的桌面应用。

25.3.1 本地文件访问的“最小权限”原则

只开放必要的目录,而非整个文件系统。

你的应用通常只需要访问几个固定位置:用户的文档目录、应用自己的数据目录、临时目录。推荐使用 Electron 提供的 app.getPath(name) 获取标准系统路径,而不是由用户或输入任意指定路径。

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

// 推荐的目录获取方式
const userDataPath = app.getPath('userData');    // 应用专属数据目录
const documentsPath = app.getPath('documents');  // 用户文档目录
const tempPath = app.getPath('temp');            // 系统临时目录

当需要通过对话框让用户选择文件时,使用原生的 dialog.showOpenDialog,并明确限制可选的文件类型和默认路径,避免用户被引导至系统敏感目录。

const { dialog } = require('electron');

const result = await dialog.showOpenDialog({
  defaultPath: app.getPath('documents'),
  filters: [{ name: '文档', extensions: ['md', 'txt'] }],
  properties: ['openFile']
});

禁止将用户提供的路径直接拼接到文件操作中。 即使路径来自对话框,也应该做一层归一化和范围校验,防止符号链接绕过或路径遍历攻击。

const safePath = path.resolve(userDataPath, userInput);
if (!safePath.startsWith(userDataPath)) {
  throw new Error('非法路径访问');
}

25.3.2 敏感数据的加密存储

用户的密码、Token、密钥等敏感信息绝不能以明文形式写入文件。Node.js 提供了 crypto 模块,配合系统级的密钥存储方案(如 Windows DPAPI、macOS Keychain、Linux libsecret)可以构建安全且用户友好的存储方案。

推荐方案:使用 safeStorage API(Electron 15+)

safeStorage 是 Electron 内置的加密存储接口,它会利用当前操作系统的原生加密机制来保护数据,例如 macOS 的 Keychain 和 Windows 的 DPAPI。加密后的字符串可以安全地写入本地文件,只有在同一台机器的同一用户账户下才能解密。

const { safeStorage } = require('electron');

// 加密
const plainText = 'my-secret-token';
const encrypted = safeStorage.encryptString(plainText);
fs.writeFileSync('secret.bin', encrypted);

// 解密
const buffer = fs.readFileSync('secret.bin');
const decrypted = safeStorage.decryptString(buffer);
console.log(decrypted); // 'my-secret-token'

如果必须使用自定义加密(例如数据需要跨端同步),至少应采用 AES-256-GCM 等成熟算法,并将密钥交由系统凭据管理器保管,而不是硬编码在代码中或写在配置文件里。

安全存储 checklist:

  • 永远不在代码仓库中提交任何密钥或种子。
  • 身份令牌存储不落盘为明文 JSON。
  • 使用 crypto.randomBytes 生成随机初始化向量(IV),不要使用固定值。
  • 即使数据存储在本地 SQLite 中,敏感字段也应加密后存入。

25.3.3 防止路径遍历与注入

路径遍历攻击(Path Traversal)是桌面应用最容易忽视的攻击面。如果渲染进程可以通过某种方式向主进程传递文件名,且主进程未做校验就直接拼接到文件路径中,攻击者可能通过 ../../etc/passwd 这样的输入读取任意系统文件。

防护措施:

  1. 永远不在渲染进程中暴露 fs 等 Node.js 模块。 使用 contextBridge 提供有限的、经过校验的 API 接口。渲染进程只能通过 IPC 请求主进程执行文件操作,且主进程必须严格校验传入的文件名。
  1. 路径归一化与白名单校验。 使用 path.resolve 将路径解析为绝对路径,然后检查其是否在预期的目录范围内。
function isPathWithin(allowedBase, targetPath) {
  const resolved = path.resolve(allowedBase, targetPath);
  return resolved.startsWith(allowedBase);
}
  1. 使用正则或白名单过滤文件名。 只允许字母、数字、下划线、短横线和点,并拒绝包含 ../\ 的输入。

25.3.4 临时文件的清理

应用运行过程中生成的临时文件如果长时间不清理,不仅浪费磁盘空间,还可能泄露历史敏感数据(如导出的报表、预览图片等)。建议遵循以下原则:

  • 使用 app.getPath('temp') 存放临时文件,系统在一定条件下会自行清理。
  • 文件使用完毕后立即删除,或监听应用退出事件 app.on('will-quit') 进行统一清理。
  • 敏感临时文件在删除前应用随机数据覆写(crypto.randomBytes 写入后再删除),尽管这对 SSD 来说并不绝对安全,但至少增加恢复难度。

25.3.5 用户数据的隔离与备份

每个 Electron 应用都有独立的 userData 目录,这本身已经做到了应用级隔离。但你的应用内部还应注意:

  • 按用户账号隔离数据:如果应用支持多用户登录,不同用户的信息应存放在不同子目录或不同数据库中,且切换用户时及时清理内存中的旧数据。
  • 配置文件权限:在 Unix-like 系统上,用户数据目录的权限应设置为 0700(仅所有者可读)。Electron 创建 userData 目录时通常会自动配置合理权限,但如果你的应用手动创建子目录,记得显式设置:
fs.mkdirSync(dir, { mode: 0o700 });
  • 定期备份用户配置:对于笔记类、编辑类应用,可以在应用启动或保存时自动将关键文件备份到用户文档目录,或提供一键导出功能。这既提升了数据安全,也增强了用户信任。

25.3.6 日志与调试信息的安全红线

开发期我们习惯将大量信息输出到控制台或日志文件,但最终交付给用户的版本中,必须严格审视日志内容:

  • 绝不记录明文密码、Token、密钥。
  • 避免记录完整文件路径(只记录相对路径或脱敏后的路径片段),这有助于防止本地信息泄露被其他进程利用。
  • 关闭生产环境的 DevTools 和调试开关。 即使用户手动打开 DevTools,也不应暴露敏感变量或允许通过控制台执行危险操作。

你可以在主进程启动时配置日志级别,并在生产环境中禁用 console.log 的输出或将其重定向到加密日志文件。

25.3.7 安全习惯 checklist

每次发布前,确认以下检查项:

  • [ ] 渲染进程是否可以直接调用 fschild_process?应禁用 nodeIntegration 且推荐 contextIsolation: true
  • [ ] 所有文件读写操作是否都经过了路径范围校验?
  • [ ] 密码和 Token 是否使用了 safeStorage 加密存储?
  • [ ] 临时文件是否在使用后及时删除?
  • [ ] 依赖的第三方 npm 包是否经过审查,是否存在恶意文件操作代码?
  • [ ] 用户数据目录权限是否合理?
  • [ ] 生产构建是否移除了所有调试输出和开发者工具入口?

本地文件与用户数据安全不是一次性的配置,而是贯穿开发全周期的持续关注点。遵循以上规范,你的 Electron 应用就能够在为用户提供强大离线能力的同时,筑起一道可靠的隐私防线。