security: block apply_patch path traversal outside workspace (#16405)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0fcd3f8c3a
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-14 19:11:12 +00:00
committed by GitHub
parent 4734f99108
commit 5544646a09
3 changed files with 77 additions and 28 deletions

View File

@@ -70,4 +70,72 @@ describe("applyPatch", () => {
expect(contents).toBe("line1\nline2\n");
});
});
it("rejects path traversal outside cwd", async () => {
await withTempDir(async (dir) => {
const escapedPath = path.join(path.dirname(dir), "escaped.txt");
const relativeEscape = path.relative(dir, escapedPath);
const patch = `*** Begin Patch
*** Add File: ${relativeEscape}
+escaped
*** End Patch`;
await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow(/Path escapes sandbox root/);
await expect(fs.readFile(escapedPath, "utf8")).rejects.toBeDefined();
});
});
it("rejects absolute paths outside cwd", async () => {
await withTempDir(async (dir) => {
const escapedPath = path.join(os.tmpdir(), `openclaw-apply-patch-${Date.now()}.txt`);
const patch = `*** Begin Patch
*** Add File: ${escapedPath}
+escaped
*** End Patch`;
try {
await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow(/Path escapes sandbox root/);
await expect(fs.readFile(escapedPath, "utf8")).rejects.toBeDefined();
} finally {
await fs.rm(escapedPath, { force: true });
}
});
});
it("allows absolute paths within cwd", async () => {
await withTempDir(async (dir) => {
const target = path.join(dir, "nested", "inside.txt");
const patch = `*** Begin Patch
*** Add File: ${target}
+inside
*** End Patch`;
await applyPatch(patch, { cwd: dir });
const contents = await fs.readFile(target, "utf8");
expect(contents).toBe("inside\n");
});
});
it("rejects symlink escape attempts", async () => {
await withTempDir(async (dir) => {
const outside = path.join(path.dirname(dir), "outside-target.txt");
const linkPath = path.join(dir, "link.txt");
await fs.writeFile(outside, "initial\n", "utf8");
await fs.symlink(outside, linkPath);
const patch = `*** Begin Patch
*** Update File: link.txt
@@
-initial
+pwned
*** End Patch`;
await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow(/Symlink not allowed/);
const outsideContents = await fs.readFile(outside, "utf8");
expect(outsideContents).toBe("initial\n");
await fs.rm(outside, { force: true });
});
});
});