fix(security): add optional workspace-only path guards for fs tools

This commit is contained in:
Peter Steinberger
2026-02-14 23:50:04 +01:00
parent 55a25f9875
commit 5e7c3250cb
14 changed files with 201 additions and 25 deletions

View File

@@ -48,7 +48,7 @@ export function resolveSandboxPath(params: { filePath: string; cwd: string; root
export async function assertSandboxPath(params: { filePath: string; cwd: string; root: string }) {
const resolved = resolveSandboxPath(params);
await assertNoSymlink(resolved.relative, path.resolve(params.root));
await assertNoSymlinkEscape(resolved.relative, path.resolve(params.root));
return resolved;
}
@@ -86,10 +86,11 @@ export async function resolveSandboxedMediaSource(params: {
return resolved.resolved;
}
async function assertNoSymlink(relative: string, root: string) {
async function assertNoSymlinkEscape(relative: string, root: string) {
if (!relative) {
return;
}
const rootReal = await tryRealpath(root);
const parts = relative.split(path.sep).filter(Boolean);
let current = root;
for (const part of parts) {
@@ -97,7 +98,13 @@ async function assertNoSymlink(relative: string, root: string) {
try {
const stat = await fs.lstat(current);
if (stat.isSymbolicLink()) {
throw new Error(`Symlink not allowed in sandbox path: ${current}`);
const target = await tryRealpath(current);
if (!isPathInside(rootReal, target)) {
throw new Error(
`Symlink escapes sandbox root (${shortPath(rootReal)}): ${shortPath(current)}`,
);
}
current = target;
}
} catch (err) {
const anyErr = err as { code?: string };
@@ -109,6 +116,22 @@ async function assertNoSymlink(relative: string, root: string) {
}
}
async function tryRealpath(value: string): Promise<string> {
try {
return await fs.realpath(value);
} catch {
return path.resolve(value);
}
}
function isPathInside(root: string, target: string): boolean {
const relative = path.relative(root, target);
if (!relative || relative === "") {
return true;
}
return !(relative.startsWith("..") || path.isAbsolute(relative));
}
function shortPath(value: string) {
if (value.startsWith(os.homedir())) {
return `~${value.slice(os.homedir().length)}`;