Files
STCourses/packages/feature-import/test/hostile-zip.test.ts
T

40 lines
1.8 KiB
TypeScript

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
});