@@ -409,6 +409,38 @@ describe("applyPatch", () => {
|
409 | 409 | }); |
410 | 410 | }); |
411 | 411 | |
| 412 | +it("rejects move targets whose parent path is a symlink outside cwd", async () => { |
| 413 | +if (process.platform === "win32") { |
| 414 | +return; |
| 415 | +} |
| 416 | +await withTempDir(async (dir) => { |
| 417 | +const outsideDir = await fs.mkdtemp(path.join(path.dirname(dir), "openclaw-patch-outside-")); |
| 418 | +try { |
| 419 | +const sourcePath = path.join(dir, "source.txt"); |
| 420 | +const outsideTarget = path.join(outsideDir, "moved.txt"); |
| 421 | +const linkDir = path.join(dir, "link"); |
| 422 | +await fs.writeFile(sourcePath, "before\n", "utf8"); |
| 423 | +await fs.symlink(outsideDir, linkDir); |
| 424 | + |
| 425 | +const patch = `*** Begin Patch |
| 426 | +*** Update File: source.txt |
| 427 | +*** Move to: link/moved.txt |
| 428 | +@@ |
| 429 | +-before |
| 430 | ++after |
| 431 | +*** End Patch`; |
| 432 | + |
| 433 | +await expect(applyPatch(patch, { cwd: dir })).rejects.toThrow( |
| 434 | +/path alias under sandbox root|symlink escapes sandbox root/i, |
| 435 | +); |
| 436 | +await expect(fs.readFile(sourcePath, "utf8")).resolves.toBe("before\n"); |
| 437 | +await expectMissingPath(fs.readFile(outsideTarget, "utf8")); |
| 438 | +} finally { |
| 439 | +await fs.rm(outsideDir, { recursive: true, force: true }); |
| 440 | +} |
| 441 | +}); |
| 442 | +}); |
| 443 | + |
412 | 444 | it.runIf(process.platform !== "win32")( |
413 | 445 | "does not delete out-of-root files when a checked directory is rebound before remove", |
414 | 446 | async () => { |
|