From 1ae216341341ec33690ad4210eb8cb1d24b792f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 23:45:36 +0000 Subject: [PATCH] test: tighten install safe path coverage --- src/infra/install-safe-path.test.ts | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/infra/install-safe-path.test.ts b/src/infra/install-safe-path.test.ts index 61ac64a2126..7cd9498e5d1 100644 --- a/src/infra/install-safe-path.test.ts +++ b/src/infra/install-safe-path.test.ts @@ -35,6 +35,12 @@ describe("safePathSegmentHashed", () => { expect(safePathSegmentHashed("demo-skill")).toBe("demo-skill"); }); + it("falls back to a hashed skill name for empty or dot-like segments", () => { + expect(safePathSegmentHashed(" ")).toMatch(/^skill-[a-f0-9]{10}$/); + expect(safePathSegmentHashed(".")).toMatch(/^skill-[a-f0-9]{10}$/); + expect(safePathSegmentHashed("..")).toMatch(/^skill-[a-f0-9]{10}$/); + }); + it("normalizes separators and adds hash suffix", () => { const result = safePathSegmentHashed("../../demo/skill"); expect(result.includes("/")).toBe(false); @@ -96,6 +102,57 @@ describe("assertCanonicalPathWithinBase", () => { } }); + it("accepts missing candidate paths when their parent stays in base", async () => { + const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-")); + try { + const candidate = path.join(baseDir, "tools", "plugin"); + await fs.mkdir(path.dirname(candidate), { recursive: true }); + await expect( + assertCanonicalPathWithinBase({ + baseDir, + candidatePath: candidate, + boundaryLabel: "install directory", + }), + ).resolves.toBeUndefined(); + } finally { + await fs.rm(baseDir, { recursive: true, force: true }); + } + }); + + it("rejects non-directory base paths", async () => { + const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-")); + const baseFile = path.join(baseDir, "not-a-dir"); + await fs.writeFile(baseFile, "nope", "utf-8"); + try { + await expect( + assertCanonicalPathWithinBase({ + baseDir: baseFile, + candidatePath: path.join(baseFile, "child"), + boundaryLabel: "install directory", + }), + ).rejects.toThrow(/base directory must be a real directory/i); + } finally { + await fs.rm(baseDir, { recursive: true, force: true }); + } + }); + + it("rejects non-directory candidate paths inside the base", async () => { + const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-")); + const candidate = path.join(baseDir, "file.txt"); + await fs.writeFile(candidate, "nope", "utf-8"); + try { + await expect( + assertCanonicalPathWithinBase({ + baseDir, + candidatePath: candidate, + boundaryLabel: "install directory", + }), + ).rejects.toThrow(/must stay within install directory/i); + } finally { + await fs.rm(baseDir, { recursive: true, force: true }); + } + }); + it.runIf(process.platform !== "win32")( "rejects symlinked candidate directories that escape the base", async () => { @@ -117,4 +174,23 @@ describe("assertCanonicalPathWithinBase", () => { } }, ); + + it.runIf(process.platform !== "win32")("rejects symlinked base directories", async () => { + const parentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-safe-")); + const realBaseDir = path.join(parentDir, "real-base"); + const symlinkBaseDir = path.join(parentDir, "base-link"); + await fs.mkdir(realBaseDir, { recursive: true }); + await fs.symlink(realBaseDir, symlinkBaseDir); + try { + await expect( + assertCanonicalPathWithinBase({ + baseDir: symlinkBaseDir, + candidatePath: path.join(symlinkBaseDir, "tool"), + boundaryLabel: "install directory", + }), + ).rejects.toThrow(/base directory must be a real directory/i); + } finally { + await fs.rm(parentDir, { recursive: true, force: true }); + } + }); });