From efa67b6e24942e0f9c3ac90c7878275a206e99a4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 11 May 2026 03:54:20 +0100 Subject: [PATCH] test: tighten archive security assertions --- src/infra/archive.test.ts | 52 +++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/infra/archive.test.ts b/src/infra/archive.test.ts index eea7141a702..6016d2f65ee 100644 --- a/src/infra/archive.test.ts +++ b/src/infra/archive.test.ts @@ -55,8 +55,23 @@ async function createDirectorySymlink(targetDir: string, linkPath: string) { await fs.symlink(targetDir, linkPath, directorySymlinkType); } +async function expectRejectedCode(promise: Promise, expected: string | RegExp) { + try { + await promise; + } catch (error) { + const code = (error as Partial).code; + if (typeof expected === "string") { + expect(code).toBe(expected); + return; + } + expect(String(code)).toMatch(expected); + return; + } + throw new Error("expected promise to reject"); +} + async function expectPathMissing(filePath: string) { - await expect(fs.stat(filePath)).rejects.toMatchObject({ code: "ENOENT" }); + await expectRejectedCode(fs.stat(filePath), "ENOENT"); } async function expectExtractedSizeBudgetExceeded(params: { @@ -148,15 +163,14 @@ describe("archive utils", () => { await fs.rm(extractDir, { recursive: true, force: true }); await createDirectorySymlink(realExtractDir, extractDir); - await expect( + await expectRejectedCode( extractArchive({ archivePath, destDir: extractDir, timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, }), - ).rejects.toMatchObject({ - code: "destination-symlink", - } satisfies Partial); + "destination-symlink", + ); await expectPathMissing(path.join(realExtractDir, "package", "hello.txt")); }); @@ -189,15 +203,14 @@ describe("archive utils", () => { zip.file("escape/pwn.txt", "owned"); await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" })); - await expect( + await expectRejectedCode( extractArchive({ archivePath, destDir: extractDir, timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, }), - ).rejects.toMatchObject({ - code: "destination-symlink-traversal", - } satisfies Partial); + "destination-symlink-traversal", + ); const outsideFile = path.join(outsideDir, "pwn.txt"); const outsideExists = await fs @@ -239,9 +252,8 @@ describe("archive utils", () => { }); } catch (error) { rejected = true; - expect(error).toMatchObject({ - code: expect.stringMatching(/destination-symlink-traversal|not-file/), - } satisfies Partial); + const code = (error as Partial).code; + expect(String(code)).toMatch(/destination-symlink-traversal|not-file/); } await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("SAFE"); @@ -280,21 +292,20 @@ describe("archive utils", () => { }); try { - await expect( + await expectRejectedCode( extractArchive({ archivePath, destDir: extractDir, timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, }), - ).rejects.toMatchObject({ - code: expect.stringMatching(/^(?:destination-symlink-traversal|hardlink)$/u), - }); + /^(?:destination-symlink-traversal|hardlink)$/u, + ); } finally { lstatSpy.mockRestore(); } await expect(fs.readFile(outsideAlias, "utf8")).resolves.toBe(""); - await expect(fs.stat(extractedPath)).rejects.toMatchObject({ code: "ENOENT" }); + await expectPathMissing(extractedPath); }); }, ); @@ -327,15 +338,14 @@ describe("archive utils", () => { await createDirectorySymlink(outsideDir, path.join(extractDir, "escape")); await tar.c({ cwd: archiveRoot, file: archivePath }, ["escape"]); - await expect( + await expectRejectedCode( extractArchive({ archivePath, destDir: extractDir, timeoutMs: ARCHIVE_EXTRACT_TIMEOUT_MS, }), - ).rejects.toMatchObject({ - code: "destination-symlink-traversal", - } satisfies Partial); + "destination-symlink-traversal", + ); await expectPathMissing(path.join(outsideDir, "pwn.txt")); });