fix(security): harden archive extraction (#16203)

* fix(browser): confine upload paths for file chooser

* fix(browser): sanitize suggested download filenames

* chore(lint): avoid control regex in download sanitizer

* test(browser): cover absolute escape paths

* docs(browser): update upload example path

* refactor(browser): centralize upload path confinement

* fix(infra): harden tmp dir selection

* fix(security): harden archive extraction

* fix(infra): harden tar extraction filter
This commit is contained in:
Peter Steinberger
2026-02-14 14:42:08 +01:00
committed by GitHub
parent 9a134c8a10
commit 3aa94afcfd
19 changed files with 1179 additions and 100 deletions

View File

@@ -49,6 +49,21 @@ describe("archive utils", () => {
expect(content).toBe("hi");
});
it("rejects zip path traversal (zip slip)", async () => {
const workDir = await makeTempDir();
const archivePath = path.join(workDir, "bundle.zip");
const extractDir = path.join(workDir, "a");
const zip = new JSZip();
zip.file("../b/evil.txt", "pwnd");
await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" }));
await fs.mkdir(extractDir, { recursive: true });
await expect(
extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }),
).rejects.toThrow(/(escapes destination|absolute)/i);
});
it("extracts tar archives", async () => {
const workDir = await makeTempDir();
const archivePath = path.join(workDir, "bundle.tar");
@@ -65,4 +80,20 @@ describe("archive utils", () => {
const content = await fs.readFile(path.join(rootDir, "hello.txt"), "utf-8");
expect(content).toBe("yo");
});
it("rejects tar path traversal (zip slip)", async () => {
const workDir = await makeTempDir();
const archivePath = path.join(workDir, "bundle.tar");
const extractDir = path.join(workDir, "extract");
const insideDir = path.join(workDir, "inside");
await fs.mkdir(insideDir, { recursive: true });
await fs.writeFile(path.join(workDir, "outside.txt"), "pwnd");
await tar.c({ cwd: insideDir, file: archivePath }, ["../outside.txt"]);
await fs.mkdir(extractDir, { recursive: true });
await expect(
extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }),
).rejects.toThrow(/escapes destination/i);
});
});