fix(agents): block workspaceOnly apply_patch delete symlink escape

This commit is contained in:
Peter Steinberger
2026-02-15 03:23:16 +01:00
parent 683aa09b55
commit 914b9d1e79
4 changed files with 52 additions and 8 deletions

View File

@@ -181,9 +181,7 @@ describe("applyPatch", () => {
*** End Patch`;
try {
await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow(
/Symlink escapes sandbox root/,
);
await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow(/Symlink escapes sandbox root/);
const stillThere = await fs.readFile(outsideFile, "utf8");
expect(stillThere).toBe("victim\n");
} finally {
@@ -216,4 +214,29 @@ describe("applyPatch", () => {
}
});
});
it("allows deleting a symlink itself even if it points outside cwd", async () => {
await withTempDir(async (dir) => {
const outsideDir = await fs.mkdtemp(path.join(path.dirname(dir), "openclaw-patch-outside-"));
try {
const outsideTarget = path.join(outsideDir, "target.txt");
await fs.writeFile(outsideTarget, "keep\n", "utf8");
const linkDir = path.join(dir, "link");
await fs.symlink(outsideDir, linkDir);
const patch = `*** Begin Patch
*** Delete File: link
*** End Patch`;
const result = await applyPatch(patch, { cwd: dir });
expect(result.summary.deleted).toEqual(["link"]);
await expect(fs.lstat(linkDir)).rejects.toBeDefined();
const outsideContents = await fs.readFile(outsideTarget, "utf8");
expect(outsideContents).toBe("keep\n");
} finally {
await fs.rm(outsideDir, { recursive: true, force: true });
}
});
});
});