This commit is contained in:
2026-10-16 16:34:15 +08:00
parent 7a3769670b
commit ff366a4daf
80 changed files with 9961 additions and 888 deletions

View File

@@ -0,0 +1,80 @@
// main/file-ops.js + main/config-store.js tmp 文件权限(audit R5 P1-13)
//
// 背景:tmp 文件用 fs.open(path, 'w') 创建时,Node 默认 mode = 0o666 受
// umask 影响(典型 0o022 → 实际 0o644),多用户 Linux/macOS 上其他用户能
// 读父目录(典型 ~/Library/Application Support/Notes)就能读 tmp,里面
// 可能含 AI Key 包络 / 整份配置 / 用户笔记内容。
//
// 修复:fs.open(tmpPath, 'w', 0o600) 强制 rw owner only。
//
// 测试策略:直接调 fs.promises.open 验证 0o600 mode 生效(file-ops 内部用
// 的就是这条路径)。atomicWriteFile 端到端测试在 file-ops.test.js 已经覆盖
// 写盘正确性,本文件专注「mode 真的生效」这条安全属性。
// @vitest-environment node
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { open } from 'node:fs/promises';
let workDir;
beforeEach(() => {
workDir = mkdtempSync(join(tmpdir(), 'notes-tmp-perms-'));
});
afterEach(() => {
if (workDir) rmSync(workDir, { recursive: true, force: true });
});
describe('tmp 文件权限 0o600(audit R5 P1-13)', () => {
it('fs.open(path, "w", 0o600) → mode & 0o077 === 0', async () => {
// Windows NTFS 不强制 POSIX 权限位(用 ACL),statSync.mode 读出来仍是
// 默认 ACL 简化位。修复在 Windows 上是 no-op(但语义一致,无害)。
// 这条断言只验证 Linux/macOS —— 真正受多用户威胁面影响的平台。
if (process.platform === 'win32') return;
const tmpPath = join(workDir, 'with-mode.tmp');
const fh = await open(tmpPath, 'w', 0o600);
await fh.writeFile('test', 'utf-8');
await fh.close();
const st = statSync(tmpPath);
// 0o600 = rw------- :owner r/w,其他全无
expect(st.mode & 0o077).toBe(0);
expect(st.mode & 0o700).toBe(0o600);
});
it('fs.open 缺 mode(fallback to 0o666 + umask)可能非 0o600 —— 回归保护', async () => {
// Windows 上 NTFS 不强制 POSIX 权限位(用 ACL),跳过
if (process.platform === 'win32') return;
const tmpPath = join(workDir, 'no-mode.tmp');
const fh = await open(tmpPath, 'w');
await fh.writeFile('test', 'utf-8');
await fh.close();
const st = statSync(tmpPath);
// 不传 mode → 默认 0o666 受 umask 影响。
// umask 0o022 → 实际 0o644 (& 0o077 = 0o044)
// umask 0o077 → 实际 0o610 (& 0o077 = 0)
// 不强断言非 0,只把 mode 读出来供 review(不传 mode 是漏洞)
// eslint-disable-next-line no-console
console.log(`[tmp-perms] no-mode default mode = 0o${(st.mode & 0o777).toString(8)} (umask-dependent)`);
expect(st).toBeDefined();
});
it('file-ops atomicWriteFile 写盘成功 + 目标文件最终权限 0o600(rename 不改 mode)', async () => {
const fileOps = await import('../../main/file-ops.js');
const target = join(workDir, 'note.md');
await fileOps.atomicWriteFile(target, '# hello\n', 'utf-8');
// rename 在 Linux 上保留 source inode 的 mode,所以 final 文件的 mode
// 也应该是 0o600(除非 umask 让 0o666 & 0o600 之后又变 —— 不会,
// 因为 0o600 显式设的)。Windows 上跳过 mode 断言。
const st = statSync(target);
if (process.platform !== 'win32') {
expect(st.mode & 0o077).toBe(0);
}
// 写盘内容正确(与权限无关,但顺手验一下不破坏)
const { readFileSync } = await import('node:fs');
expect(readFileSync(target, 'utf-8')).toBe('# hello\n');
});
});