From 842499d6c57766e9f4f4f88d6647cb72edbf2d61 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 14:53:33 +0100 Subject: [PATCH] test(security): reject hook archives with traversal entries (#16224) --- src/hooks/install.test.ts | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/hooks/install.test.ts b/src/hooks/install.test.ts index 7c15586bf5..2c9de9cf94 100644 --- a/src/hooks/install.test.ts +++ b/src/hooks/install.test.ts @@ -101,6 +101,27 @@ describe("installHooksFromArchive", () => { expect(fs.existsSync(path.join(result.targetDir, "hooks", "zip-hook", "HOOK.md"))).toBe(true); }); + it("rejects zip archives with traversal entries", async () => { + const stateDir = makeTempDir(); + const workDir = makeTempDir(); + const archivePath = path.join(workDir, "traversal.zip"); + + const zip = new JSZip(); + zip.file("../pwned.txt", "nope\n"); + const buffer = await zip.generateAsync({ type: "nodebuffer" }); + fs.writeFileSync(archivePath, buffer); + + const hooksDir = path.join(stateDir, "hooks"); + const result = await installHooksFromArchive({ archivePath, hooksDir }); + + expect(result.ok).toBe(false); + if (result.ok) { + return; + } + expect(result.error).toContain("failed to extract archive"); + expect(result.error).toContain("archive entry"); + }); + it("installs hook packs from tar archives", async () => { const stateDir = makeTempDir(); const workDir = makeTempDir(); @@ -149,6 +170,28 @@ describe("installHooksFromArchive", () => { expect(result.targetDir).toBe(path.join(stateDir, "hooks", "tar-hooks")); }); + it("rejects tar archives with traversal entries", async () => { + const stateDir = makeTempDir(); + const workDir = makeTempDir(); + const insideDir = path.join(workDir, "inside"); + fs.mkdirSync(insideDir, { recursive: true }); + + // Create a tar that contains a ../ entry; extract must fail closed. + fs.writeFileSync(path.join(workDir, "outside.txt"), "nope\n", "utf-8"); + const archivePath = path.join(workDir, "traversal.tar"); + await tar.c({ cwd: insideDir, file: archivePath }, ["../outside.txt"]); + + const hooksDir = path.join(stateDir, "hooks"); + const result = await installHooksFromArchive({ archivePath, hooksDir }); + + expect(result.ok).toBe(false); + if (result.ok) { + return; + } + expect(result.error).toContain("failed to extract archive"); + expect(result.error).toContain("escapes destination"); + }); + it("rejects hook packs with traversal-like ids", async () => { const stateDir = makeTempDir(); const workDir = makeTempDir();