fix: harden sandboxed patch parent paths

This commit is contained in:
Peter Steinberger
2026-05-06 04:43:53 +01:00
parent cbc228f0f6
commit 777c539daf
2 changed files with 51 additions and 1 deletions

View File

@@ -452,7 +452,7 @@ describe("applyPatch", () => {
timing: "before-realpath",
run: async () => {
await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow(
/under root|unable to resolve opened file path/i,
/path alias under sandbox root|path escapes sandbox root|under root|unable to resolve opened file path/i,
);
},
});

View File

@@ -152,6 +152,7 @@ export async function applyPatch(
if (hunk.kind === "add") {
const target = await resolvePatchPath(hunk.path, options);
await assertPatchParentPath(hunk.path, options);
await ensureDir(target.resolved, fileOps);
await fileOps.writeFile(target.resolved, hunk.contents);
recordSummary(summary, seen, "added", target.display);
@@ -172,6 +173,7 @@ export async function applyPatch(
if (hunk.movePath) {
const moveTarget = await resolvePatchPath(hunk.movePath, options);
await assertPatchParentPath(hunk.movePath, options);
await ensureDir(moveTarget.resolved, fileOps);
await fileOps.writeFile(moveTarget.resolved, applied);
await fileOps.remove(target.resolved);
@@ -301,6 +303,54 @@ async function ensureDir(filePath: string, ops: PatchFileOps) {
await ops.mkdirp(parent);
}
async function assertPatchParentPath(filePath: string, options: ApplyPatchOptions) {
if (options.workspaceOnly === false || options.sandbox) {
return;
}
const parent = path.dirname(filePath);
if (!parent || parent === ".") {
return;
}
await assertSandboxPath({
filePath: parent,
cwd: options.cwd,
root: options.cwd,
});
await assertNoExistingParentAliases({
parentPath: resolvePathFromInput(parent, options.cwd),
rootPath: options.cwd,
});
}
async function assertNoExistingParentAliases(params: { parentPath: string; rootPath: string }) {
const rootPath = path.resolve(params.rootPath);
const parentPath = path.resolve(params.parentPath);
const relative = path.relative(rootPath, parentPath);
if (!relative || relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
return;
}
let current = rootPath;
for (const segment of relative.split(path.sep)) {
if (!segment) {
continue;
}
current = path.join(current, segment);
const stat = await fs.lstat(current).catch((error: unknown) => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return null;
}
throw error;
});
if (!stat) {
return;
}
if (stat.isSymbolicLink()) {
throw new Error(`Path alias under sandbox root: ${path.relative(rootPath, current)}`);
}
}
}
async function resolvePatchPath(
filePath: string,
options: ApplyPatchOptions,