test: tighten archive security assertions

This commit is contained in:
Peter Steinberger
2026-05-11 03:54:20 +01:00
parent 369bed9639
commit efa67b6e24

View File

@@ -55,8 +55,23 @@ async function createDirectorySymlink(targetDir: string, linkPath: string) {
await fs.symlink(targetDir, linkPath, directorySymlinkType);
}
async function expectRejectedCode(promise: Promise<unknown>, expected: string | RegExp) {
try {
await promise;
} catch (error) {
const code = (error as Partial<ArchiveSecurityError>).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<ArchiveSecurityError>);
"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<ArchiveSecurityError>);
"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<ArchiveSecurityError>);
const code = (error as Partial<ArchiveSecurityError>).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<ArchiveSecurityError>);
"destination-symlink-traversal",
);
await expectPathMissing(path.join(outsideDir, "pwn.txt"));
});