This commit is contained in:
2026-09-12 13:55:57 +08:00
commit 30f1cedd39
95 changed files with 42057 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
// 回归测试H10 — file:exists 必须走 assertInDataDir
//
// 背景:旧版 file:exists 直接 fs.access(filePath) —— 配合一个 XSS任何能从
// renderer 调到 api.fileExists 的注入)就能枚举整盘存在性:
// for (let p of commonPaths) { if (await api.fileExists(p)) found.push(p); }
// 任意文件存在性是隐私 + 安全敏感信息(用户的密码管理器 DB 是否存在、
// 公司加密软件安装情况等)。
//
// 修复v3.7+file:exists 与 file:write / file:read 同款走 assertInDataDir
// 数据目录外的路径 → 一律返回 false不抛错避免泄露信息
//
// 跑法node scripts/check-assert-in-data-dir.mjs
import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs/promises';
import { check, summary, printSummary } from './_lib/check.mjs';
// ── [1] assertInDataDir 行为镜像 ──
console.log('\n[1] assertInDataDir 行为:拒绝数据目录外路径');
const dataDir = path.resolve(os.homedir(), 'TodoList');
function assertInDataDir(p) {
if (typeof p !== 'string' || !p) {
throw new Error('path must be a non-empty string');
}
const root = path.resolve(dataDir);
const abs = path.resolve(p);
const rootCmp = process.platform === 'win32' ? root.toLowerCase() : root;
const absCmp = process.platform === 'win32' ? abs.toLowerCase() : abs;
if (absCmp !== rootCmp && !absCmp.startsWith(rootCmp + path.sep)) {
throw new Error('path outside data directory');
}
return abs;
}
{
check('数据目录内的合法路径 → 不抛错',
(() => { try { return assertInDataDir(path.join(dataDir, 'todo.md')); } catch { return false; } })() !== false);
check('上级目录穿越 → 抛错',
(() => { try { assertInDataDir(path.join(dataDir, '..', 'evil.txt')); return false; } catch { return true; } })());
check('绝对路径在数据目录外 → 抛错',
(() => {
const outside = process.platform === 'win32' ? 'C:\\Windows\\System32\\evil.dll' : '/etc/passwd';
try { assertInDataDir(outside); return false; } catch { return true; }
})());
check('空字符串 → 抛错',
(() => { try { assertInDataDir(''); return false; } catch { return true; } })());
check('非字符串(数字)→ 抛错',
(() => { try { assertInDataDir(123); return false; } catch { return true; } })());
check('非字符串null→ 抛错',
(() => { try { assertInDataDir(null); return false; } catch { return true; } })());
}
// ── [2] file:exists 行为:对外路径一律返回 false ──
console.log('\n[2] file:exists 模拟:对外路径一律返回 false');
/**
* 模拟 main.js 的 file:exists handlerassertInDataDir + fs.access + try/catch。
* 任意一步抛错都返回 false不向 renderer 抛错,避免泄露目录结构)。
*/
async function fileExists(filePath) {
try {
const abs = assertInDataDir(filePath);
await fs.access(abs);
return true;
} catch {
return false;
}
}
{
// 数据目录内:文件存在 → true
// 准备:临时创建数据目录内的一个文件,跑完删除
const tmpFile = path.join(dataDir, `__check-assert-${Date.now()}.tmp`);
try {
await fs.mkdir(dataDir, { recursive: true });
await fs.writeFile(tmpFile, 'hello', 'utf8');
check('数据目录内已存在的文件 → file:exists 返回 true',
await fileExists(tmpFile) === true);
} finally {
try { await fs.unlink(tmpFile); } catch { /* ignore */ }
}
// 数据目录内:文件不存在 → false不是抛错
check('数据目录内不存在的文件 → file:exists 返回 false',
await fileExists(path.join(dataDir, 'definitely-does-not-exist.md')) === false);
// 数据目录外fs.access 能探测到也必须返回 false
check('Windows 系统目录(无论是否存在)→ file:exists 返回 false防信息泄露',
await fileExists(process.platform === 'win32' ? 'C:\\Windows\\System32\\drivers\\etc\\hosts' : '/etc/passwd') === false);
// 绝对路径在数据目录外
check('数据目录外绝对路径 → file:exists 返回 false',
await fileExists(process.platform === 'win32' ? 'C:\\Users\\Public\\test.txt' : '/tmp/test.txt') === false);
// 路径穿越攻击
check('路径穿越(../evil.txt→ file:exists 返回 false',
await fileExists(path.join(dataDir, '..', 'evil.txt')) === false);
// 空字符串
check('空字符串 → file:exists 返回 false',
await fileExists('') === false);
// 非字符串
check('非字符串null→ file:exists 返回 false',
await fileExists(null) === false);
}
printSummary('check-assert-in-data-dir');