fix(worker): contain workspace paths against symlinks

This commit is contained in:
Gabriel Brown
2026-07-10 17:11:00 -04:00
parent 2bc090c378
commit 57c175aecd
4 changed files with 107 additions and 23 deletions
@@ -0,0 +1,54 @@
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, test } from 'vitest';
import { assertContainedRealPath } from '../../src/path-containment';
let dir: string;
afterEach(async () => {
if (dir) await rm(dir, { recursive: true, force: true });
});
describe('assertContainedRealPath', () => {
test('allows a normal file inside the root', async () => {
dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
const root = path.join(dir, 'repo');
await mkdir(root, { recursive: true });
await writeFile(path.join(root, 'a.txt'), 'hi');
await expect(assertContainedRealPath(root, 'a.txt')).resolves.toBe(
path.join(root, 'a.txt'),
);
});
test('rejects a lexical .. escape', async () => {
dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
const root = path.join(dir, 'repo');
await mkdir(root, { recursive: true });
await expect(assertContainedRealPath(root, '../secret')).rejects.toThrow();
});
test('rejects reading through a symlink that escapes the root', async () => {
dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
const root = path.join(dir, 'repo');
const outside = path.join(dir, 'outside');
await mkdir(root, { recursive: true });
await mkdir(outside, { recursive: true });
await writeFile(path.join(outside, 'secret'), 'S');
await symlink(outside, path.join(root, 'link'));
await expect(
assertContainedRealPath(root, 'link/secret'),
).rejects.toThrow();
});
test('rejects a write whose final target is a symlink', async () => {
dir = await mkdtemp(path.join(tmpdir(), 'spoon-'));
const root = path.join(dir, 'repo');
await mkdir(root, { recursive: true });
await symlink(path.join(dir, 'evil'), path.join(root, 'out'));
await expect(
assertContainedRealPath(root, 'out', { forWrite: true }),
).rejects.toThrow();
});
});