import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { chmodSync, mkdirSync, mkdtempSync, renameSync, rmSync, symlinkSync, writeFileSync, type PathLike, } from 'node:fs'; import type * as NodeFs from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; interface FilesystemRaceState { afterLstat?: (path: string) => void; afterOpen?: (path: string) => void; } const filesystemRaceState = vi.hoisted(() => ({})); vi.mock('node:fs', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, lstatSync: (path: PathLike) => { const result = actual.lstatSync(path); filesystemRaceState.afterLstat?.(String(path)); return result; }, openSync: (path: PathLike, flags: string | number, mode?: number) => { const fd = actual.openSync(path, flags, mode); filesystemRaceState.afterOpen?.(String(path)); return fd; }, }; }); import { assertCanonicalContainment, readRegularFileSecure } from './secure-file.js'; describe('secure file reads', () => { let root: string; beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'mosaic-secure-file-')); filesystemRaceState.afterLstat = undefined; filesystemRaceState.afterOpen = undefined; }); afterEach(() => { filesystemRaceState.afterLstat = undefined; filesystemRaceState.afterOpen = undefined; rmSync(root, { recursive: true, force: true }); }); it('rejects canonical path escape', () => { expect(() => assertCanonicalContainment(root, join(root, '..', 'outside'))).toThrow( 'path escapes managed root', ); }); it('rejects a symlink in a file ancestor', () => { const external = join(root, 'external'); mkdirSync(external); writeFileSync(join(external, 'file'), 'external\n'); symlinkSync(external, join(root, 'linked')); expect(() => readRegularFileSecure(join(root, 'linked', 'file'), { root })).toThrow( 'path ancestor is a symbolic link', ); }); it('rejects a symlink target', () => { const external = join(root, 'external'); writeFileSync(external, 'external\n'); symlinkSync(external, join(root, 'linked-file')); expect(() => readRegularFileSecure(join(root, 'linked-file'), { root })).toThrow( 'file is a symbolic link', ); }); it('keeps ancestor traversal bound when an opened directory is substituted', () => { const tools = join(root, 'tools'); const displacedTools = join(root, 'tools.displaced'); const external = join(root, 'external'); const helper = join(tools, 'helper.sh'); mkdirSync(tools); mkdirSync(external); writeFileSync(helper, 'trusted\n', { mode: 0o755 }); writeFileSync(join(external, 'helper.sh'), 'external marker\n', { mode: 0o755 }); let substituted = false; filesystemRaceState.afterOpen = (openedPath: string): void => { if (substituted || !openedPath.startsWith('/proc/self/fd/')) return; if (openedPath.split('/').at(-1) !== 'tools') return; substituted = true; renameSync(tools, displacedTools); symlinkSync(external, tools); }; const result = readRegularFileSecure(helper, { root, executable: true }); expect(substituted).toBe(true); expect(result.content.toString('utf8')).toBe('trusted\n'); }); it('keeps root selection bound when the opened root is substituted', () => { const displacedRoot = `${root}.displaced`; const externalRoot = `${root}.external`; const helper = join(root, 'helper.sh'); mkdirSync(externalRoot); writeFileSync(helper, 'trusted root\n', { mode: 0o755 }); writeFileSync(join(externalRoot, 'helper.sh'), 'external root marker\n', { mode: 0o755 }); let substituted = false; filesystemRaceState.afterOpen = (openedPath: string): void => { if (substituted || !openedPath.startsWith('/proc/self/fd/')) return; const match = openedPath.match(/\/([^/]+)$/); if (match?.[1] !== root.split('/').filter(Boolean).at(-1)) return; substituted = true; renameSync(root, displacedRoot); symlinkSync(externalRoot, root); }; const result = readRegularFileSecure(helper, { root, executable: true }); expect(substituted).toBe(true); expect(result.content.toString('utf8')).toBe('trusted root\n'); filesystemRaceState.afterOpen = undefined; rmSync(root); renameSync(displacedRoot, root); }); it('keeps target read and execute validation bound to the opened file', () => { const file = join(root, 'helper.sh'); const displaced = join(root, 'helper.displaced.sh'); const external = join(root, 'external-helper.sh'); writeFileSync(file, 'trusted target\n', { mode: 0o755 }); writeFileSync(external, 'external target marker\n', { mode: 0o755 }); let substituted = false; filesystemRaceState.afterOpen = (openedPath: string): void => { if (substituted || !openedPath.startsWith('/proc/self/fd/')) return; if (openedPath.split('/').at(-1) !== 'helper.sh') return; substituted = true; renameSync(file, displaced); symlinkSync(external, file); }; const result = readRegularFileSecure(file, { root, executable: true }); expect(substituted).toBe(true); expect(result.content.toString('utf8')).toBe('trusted target\n'); }); it('uses a stable redacted executable error while retaining the error code', () => { const file = join(root, 'helper.sh'); writeFileSync(file, '#!/bin/sh\n', { mode: 0o644 }); try { readRegularFileSecure(file, { root, executable: true }); throw new Error('expected executable validation to fail'); } catch (error) { expect(error).toMatchObject({ message: 'managed file is not executable', code: 'EACCES' }); expect(String(error)).not.toContain('/proc/self/fd/'); expect(String(error)).not.toContain(root); } }); it('uses effective-identity execute access after regular-file validation', () => { const file = join(root, 'helper.sh'); writeFileSync(file, '#!/bin/sh\n', { mode: 0o644 }); expect(() => readRegularFileSecure(file, { root, executable: true })).toThrow(); chmodSync(file, 0o755); expect(readRegularFileSecure(file, { root, executable: true }).content.toString()).toBe( '#!/bin/sh\n', ); }); });