import { expect, test } from "bun:test"; import { existsSync } from "node:fs"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; import { openSource } from "../src/util/source"; import { PYTHON, tmp } from "./fixtures"; import { run } from "../src/util/exec"; /** Builds a zip by hand with traversal, absolute and symlink entries. */ async function hostileZip(path: string) { await run([PYTHON, "-I", "-c", ` import sys, zipfile, stat z = zipfile.ZipFile(sys.argv[1], 'w') z.writestr('../../evil-up.txt', 'x') z.writestr('/tmp/stc-evil-abs.txt', 'x') z.writestr('ok/fine.txt', 'fine') i = zipfile.ZipInfo('ok/link'); i.create_system = 3; i.external_attr = (stat.S_IFLNK | 0o777) << 16 z.writestr(i, '/etc') i2 = zipfile.ZipInfo('ok/link/pwned.txt'); z.writestr(i2, 'x') z.close()`, path]); } test("hostile zip entries never write outside the extraction dir", async () => { const base = await tmp("stc-zip-"); await hostileZip(join(base, "h.zip")); const src = await openSource(join(base, "h.zip")); const dest = join(base, "a", "b", "out"); await src.extract(src.entries, dest).catch(() => {}); // unzip may warn; what matters is where files land expect(existsSync(join(base, "evil-up.txt"))).toBe(false); expect(existsSync(join(base, "a", "evil-up.txt"))).toBe(false); expect(existsSync(join(base, "..", "evil-up.txt"))).toBe(false); expect(existsSync("/tmp/stc-evil-abs.txt")).toBe(false); expect(existsSync("/etc/pwned.txt")).toBe(false); expect(await Bun.file(join(dest, "ok/fine.txt")).text()).toBe("fine"); const left: string[] = []; const walk = async (d: string) => { for (const e of await readdir(d, { withFileTypes: true })) { if (e.isSymbolicLink()) left.push(e.name); else if (e.isDirectory()) await walk(join(d, e.name)); } }; await walk(dest); expect(left).toEqual([]); // the symlink entry is dropped });