fix: block symlink escapes in skills file ops

This commit is contained in:
Lawyered
2026-02-22 00:00:13 -05:00
parent 1980d97d90
commit ccef3bb8ab
2 changed files with 118 additions and 2 deletions

View File

@@ -4,6 +4,19 @@ import path from 'path';
import { executeFileOps } from '../file-ops.js';
import { createTempDir, cleanup } from './test-helpers.js';
function shouldSkipSymlinkTests(err: unknown): boolean {
return !!(
err &&
typeof err === 'object' &&
'code' in err &&
(
(err as { code?: string }).code === 'EPERM' ||
(err as { code?: string }).code === 'EACCES' ||
(err as { code?: string }).code === 'ENOSYS'
)
);
}
describe('file-ops', () => {
let tmpDir: string;
const originalCwd = process.cwd();
@@ -90,4 +103,53 @@ describe('file-ops', () => {
expect(result.success).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
it('move rejects symlink escape to outside project root', () => {
const outsideDir = createTempDir();
try {
fs.symlinkSync(outsideDir, path.join(tmpDir, 'linkdir'));
} catch (err) {
cleanup(outsideDir);
if (shouldSkipSymlinkTests(err)) return;
throw err;
}
fs.writeFileSync(path.join(tmpDir, 'source.ts'), 'content');
const result = executeFileOps([
{ type: 'move', from: 'source.ts', to: 'linkdir/pwned.ts' },
], tmpDir);
expect(result.success).toBe(false);
expect(result.errors.some((e) => e.includes('escapes project root'))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'source.ts'))).toBe(true);
expect(fs.existsSync(path.join(outsideDir, 'pwned.ts'))).toBe(false);
cleanup(outsideDir);
});
it('delete rejects symlink escape to outside project root', () => {
const outsideDir = createTempDir();
const outsideFile = path.join(outsideDir, 'victim.ts');
fs.writeFileSync(outsideFile, 'secret');
try {
fs.symlinkSync(outsideDir, path.join(tmpDir, 'linkdir'));
} catch (err) {
cleanup(outsideDir);
if (shouldSkipSymlinkTests(err)) return;
throw err;
}
const result = executeFileOps([
{ type: 'delete', path: 'linkdir/victim.ts' },
], tmpDir);
expect(result.success).toBe(false);
expect(result.errors.some((e) => e.includes('escapes project root'))).toBe(true);
expect(fs.existsSync(outsideFile)).toBe(true);
cleanup(outsideDir);
});
});