Files
stack/packages/mosaic/src/fleet/secure-file.spec.ts
jason.woltje adad486b6f
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
fix(fleet): enforce exact comms authority (#787)
2026-07-16 00:32:23 +00:00

180 lines
6.2 KiB
TypeScript

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<FilesystemRaceState>(() => ({}));
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof NodeFs>();
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',
);
});
});