diff --git a/src/agents/apply-patch.test.ts b/src/agents/apply-patch.test.ts index d35fc74402b..479123bc82e 100644 --- a/src/agents/apply-patch.test.ts +++ b/src/agents/apply-patch.test.ts @@ -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, ); }, }); diff --git a/src/agents/apply-patch.ts b/src/agents/apply-patch.ts index 64bfed640ec..783888356ee 100644 --- a/src/agents/apply-patch.ts +++ b/src/agents/apply-patch.ts @@ -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,