import { spawnSync } from "node:child_process"; import { appendFileSync, existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, truncateSync, writeFileSync, } from "node:fs"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; const SCRIPT = path.resolve("scripts/release-telegram-candidate-archive.py"); const tempDirs: string[] = []; const tarVersion = spawnSync("tar", ["--version"], { encoding: "utf8" }); const hasGnuTar = tarVersion.status === 0 && tarVersion.stdout?.includes("GNU tar") === true; afterEach(() => { for (const directory of tempDirs.splice(0)) { rmSync(directory, { recursive: true, force: true }); } }); function makeTempDir(): string { const directory = mkdtempSync(path.join(tmpdir(), "openclaw-archive-guard-")); tempDirs.push(directory); return directory; } function runHelper(args: string[]) { return spawnSync("python3", [SCRIPT, ...args], { encoding: "utf8", maxBuffer: 1024 * 1024, }); } function expectSuccess(args: string[]) { const result = runHelper(args); expect(result.status, result.stderr).toBe(0); return result; } function expectFailure(args: string[], message: string) { const result = runHelper(args); expect(result.status, result.stdout).toBe(1); expect(result.stderr).toContain(message); return result; } function compressTar(tarPath: string): string { const archivePath = `${tarPath}.zst`; const zstdResult = spawnSync("zstd", ["-q", "-f", tarPath, "-o", archivePath], { encoding: "utf8", }); expect(zstdResult.status, zstdResult.stderr).toBe(0); return archivePath; } function makeCompressedArchive(root: string, fileSize = 32): string { const source = path.join(root, "source"); const candidate = path.join(source, "candidate"); mkdirSync(candidate, { recursive: true }); writeFileSync(path.join(source, "manifest.json"), '{"version":1}\n'); writeFileSync(path.join(candidate, "payload.bin"), Buffer.alloc(fileSize, 0x61)); const tarPath = path.join(root, "candidate.tar"); const tarResult = spawnSync("tar", ["-cf", tarPath, "-C", source, "manifest.json", "candidate"], { encoding: "utf8", env: { ...process.env, COPYFILE_DISABLE: "1" }, }); expect(tarResult.status, tarResult.stderr).toBe(0); return compressTar(tarPath); } function makeDepthFirstProducerArchive(root: string): string { const source = path.join(root, "depth-first-source"); const candidate = path.join(source, "candidate"); mkdirSync(path.join(candidate, "app"), { recursive: true }); writeFileSync(path.join(source, "manifest.json"), '{"version":1}\n'); writeFileSync(path.join(candidate, "app", "child"), "child\n"); writeFileSync(path.join(candidate, "app-routes.ts"), "routes\n"); const archivePath = path.join(root, "depth-first-producer.tar.zst"); const script = String.raw` set -euo pipefail cd "$SOURCE" LC_ALL=C tar \ --create \ --format=posix \ --sort=name \ --one-file-system \ --numeric-owner \ --owner=0 \ --group=0 \ --no-xattrs \ --no-acls \ --pax-option=delete=atime,delete=ctime \ manifest.json \ candidate | zstd -T0 -3 -q -o "$ARCHIVE" `; const result = spawnSync("bash", ["-c", script], { encoding: "utf8", env: { ...process.env, ARCHIVE: archivePath, SOURCE: source, }, }); expect(result.status, result.stderr).toBe(0); return archivePath; } function makeDeclaredExtensionArchive( root: string, kind: "gnu-longlink" | "gnu-longname" | "pax" | "pax-global", size: number, ): string { const tarPath = path.join(root, `${kind}.tar`); const python = String.raw` import sys import tarfile types = { "gnu-longlink": tarfile.GNUTYPE_LONGLINK, "gnu-longname": tarfile.GNUTYPE_LONGNAME, "pax": tarfile.XHDTYPE, "pax-global": tarfile.XGLTYPE, } header = tarfile.TarInfo("././@PaxHeader") header.type = types[sys.argv[2]] header.size = int(sys.argv[3]) with open(sys.argv[1], "wb") as output: output.write(header.tobuf(format=tarfile.USTAR_FORMAT)) `; const result = spawnSync("python3", ["-c", python, tarPath, kind, String(size)], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); return compressTar(tarPath); } function makeLongMetadataArchive( root: string, kind: "gnu-longname" | "hardlink" | "pax-path" | "symlink", ): string { const tarPath = path.join(root, `${kind}-metadata.tar`); const python = String.raw` import io import sys import tarfile kind = sys.argv[2] archive_format = tarfile.GNU_FORMAT if kind == "gnu-longname" else tarfile.PAX_FORMAT with tarfile.open(sys.argv[1], "w", format=archive_format) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) long_value = "a" * 4097 if kind in {"gnu-longname", "pax-path"}: member = tarfile.TarInfo(f"candidate/{long_value}") member.size = 1 archive.addfile(member, io.BytesIO(b"x")) else: member = tarfile.TarInfo(f"candidate/{kind}") member.type = tarfile.LNKTYPE if kind == "hardlink" else tarfile.SYMTYPE member.linkname = long_value archive.addfile(member) `; const result = spawnSync("python3", ["-c", python, tarPath, kind], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); return compressTar(tarPath); } function makeDeepSortedArchive(root: string, pathCount: number): string { const tarPath = path.join(root, "deep-sorted.tar"); const python = String.raw` import io import sys import tarfile path_count = int(sys.argv[2]) with tarfile.open(sys.argv[1], "w", format=tarfile.PAX_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) tail = ["d" for _ in range(254)] for index in range(path_count): parts = ["candidate", f"{index:04d}", *tail] for component_count in range(2, len(parts)): directory = tarfile.TarInfo("/".join(parts[:component_count])) directory.type = tarfile.DIRTYPE archive.addfile(directory) member = tarfile.TarInfo("/".join(parts)) member.size = 1 archive.addfile(member, io.BytesIO(b"x")) `; const result = spawnSync("python3", ["-c", python, tarPath, String(pathCount)], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); return compressTar(tarPath); } function makeCumulativePaxArchive(root: string): string { const tarPath = path.join(root, "cumulative-pax.tar"); const python = String.raw` import io import sys import tarfile with tarfile.open(sys.argv[1], "w", format=tarfile.PAX_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) for index in range(5): member = tarfile.TarInfo(f"candidate/pax-{index}") member.pax_headers = {"comment": "x" * 400} archive.addfile(member) `; const result = spawnSync("python3", ["-c", python, tarPath], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); return compressTar(tarPath); } function makeValidHardlinkArchive(root: string): string { const tarPath = path.join(root, "valid-hardlink.tar"); const python = String.raw` import io import sys import tarfile with tarfile.open(sys.argv[1], "w", format=tarfile.USTAR_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) target = tarfile.TarInfo("candidate/a-target.txt") target_payload = b"shared\n" target.size = len(target_payload) archive.addfile(target, io.BytesIO(target_payload)) link = tarfile.TarInfo("candidate/b-link.txt") link.type = tarfile.LNKTYPE link.linkname = target.name archive.addfile(link) `; const result = spawnSync("python3", ["-c", python, tarPath], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); return compressTar(tarPath); } function makeManyMemberTar(root: string, memberCount: number): string { const tarPath = path.join(root, "many-members.tar"); const python = String.raw` import sys import tarfile member_count = int(sys.argv[2]) if member_count < 2: raise ValueError("member count must include manifest and candidate root") with open(sys.argv[1], "wb") as output: manifest = tarfile.TarInfo("manifest.json") output.write(manifest.tobuf(format=tarfile.USTAR_FORMAT)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE output.write(root.tobuf(format=tarfile.USTAR_FORMAT)) for index in range(member_count - 2): member = tarfile.TarInfo(f"candidate/f{index:06d}") output.write(member.tobuf(format=tarfile.USTAR_FORMAT)) output.write(b"\0" * 1024) `; const result = spawnSync("python3", ["-c", python, tarPath, String(memberCount)], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); return tarPath; } function makePaxHeavyTar(root: string, memberCount: number, keyCount: number): string { const tarPath = path.join(root, "pax-heavy.tar"); const python = String.raw` import sys import tarfile member_count = int(sys.argv[2]) key_count = int(sys.argv[3]) with tarfile.open(sys.argv[1], "w", format=tarfile.PAX_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") archive.addfile(manifest) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) for index in range(member_count): member = tarfile.TarInfo(f"candidate/p{index:06d}") member.pax_headers = { f"OPENCLAW.key{key:03d}": f"{index:06d}-{key:03d}" for key in range(key_count) } archive.addfile(member) `; const result = spawnSync( "python3", ["-c", python, tarPath, String(memberCount), String(keyCount)], { encoding: "utf8", }, ); expect(result.status, result.stderr).toBe(0); return compressTar(tarPath); } function probeTarInfoCache(tarPath: string) { const launcher = String.raw` import subprocess import sys completed = subprocess.run([sys.executable, "-c", sys.argv[1], *sys.argv[2:]]) raise SystemExit(completed.returncode) `; const python = String.raw` import importlib.util import json import resource import sys import tarfile spec = importlib.util.spec_from_file_location("archive_guard", sys.argv[1]) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) count = 0 max_cached_members = 0 with tarfile.open(sys.argv[2], mode="r|", bufsize=512) as archive: while True: member, cached_members = module._next_stream_member(archive) max_cached_members = max(max_cached_members, cached_members) if archive.members: raise RuntimeError("tar metadata cache was not cleared") if member is None: break count += 1 resident_kib = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss if sys.platform == "darwin": resident_kib //= 1024 print(json.dumps({ "count": count, "maxCachedMembers": max_cached_members, "residentKiB": resident_kib, })) `; return spawnSync("python3", ["-c", launcher, python, SCRIPT, tarPath], { encoding: "utf8", maxBuffer: 1024 * 1024, }); } describe("release Telegram candidate archive guard", () => { it("is executable and accepts an internal symlink", () => { expect(statSync(SCRIPT).mode & 0o111).not.toBe(0); const root = makeTempDir(); mkdirSync(path.join(root, "target")); writeFileSync(path.join(root, "target", "value.txt"), "ok\n"); symlinkSync("target/value.txt", path.join(root, "internal-link")); const result = expectSuccess([ "validate-tree", root, "--max-entries", "10", "--max-apparent-bytes", "1024", ]); expect(JSON.parse(result.stdout)).toMatchObject({ entries: 3 }); }); it("rejects an escaping symlink", () => { const container = makeTempDir(); const root = path.join(container, "root"); mkdirSync(root); writeFileSync(path.join(container, "outside.txt"), "outside\n"); symlinkSync("../outside.txt", path.join(root, "escape")); expectFailure(["validate-tree", root], "escaping symlink"); }); it("rejects a symlink supplied as the tree root", () => { const container = makeTempDir(); const target = path.join(container, "target"); const root = path.join(container, "root-link"); mkdirSync(target); writeFileSync(path.join(target, "value.txt"), "outside\n"); symlinkSync("target", root); expectFailure(["validate-tree", root], "tree root must not be a symlink"); }); it("rejects a dangling symlink", () => { const root = makeTempDir(); symlinkSync("missing.txt", path.join(root, "dangling")); expectFailure(["validate-tree", root], "dangling symlink"); }); it("rejects a socket entry", async () => { const root = makeTempDir(); const socketPath = path.join(root, "candidate.sock"); const server = createServer(); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(socketPath, resolve); }); try { expectFailure(["validate-tree", root], "unsupported special entry"); } finally { await new Promise((resolve) => server.close(() => resolve())); } }); it("uses apparent size when rejecting a sparse file", () => { const root = makeTempDir(); const sparsePath = path.join(root, "sparse.bin"); writeFileSync(sparsePath, ""); truncateSync(sparsePath, 2 * 1024 * 1024); expectFailure( ["validate-tree", root, "--max-apparent-bytes", `${1024 * 1024}`], "apparent size exceeds", ); }); it("rejects a tree over the entry-count cap", () => { const root = makeTempDir(); writeFileSync(path.join(root, "one.txt"), "one\n"); writeFileSync(path.join(root, "two.txt"), "two\n"); expectFailure(["validate-tree", root, "--max-entries", "1"], "entry count exceeds 1"); }); it("rejects a same-device hard link whose other name is outside the tree", () => { const container = makeTempDir(); const root = path.join(container, "root"); const outside = path.join(container, "outside.txt"); mkdirSync(root); writeFileSync(outside, "outside\n"); linkSync(outside, path.join(root, "linked.txt")); expectFailure(["validate-tree", root], "hard links outside the validated root"); }); it("accepts hard links whose complete link set is inside the tree", () => { const root = makeTempDir(); const first = path.join(root, "first.txt"); writeFileSync(first, "shared\n"); linkSync(first, path.join(root, "second.txt")); const result = expectSuccess(["validate-tree", root]); expect(JSON.parse(result.stdout)).toMatchObject({ entries: 2 }); }); it("streams and extracts a valid compressed archive", () => { const root = makeTempDir(); const archive = makeCompressedArchive(root); const destination = path.join(root, "extracted"); const result = expectSuccess([ "extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-members", "10", "--max-expanded-bytes", "4096", "--max-stream-bytes", `${1024 * 1024}`, ]); expect(JSON.parse(result.stdout)).toMatchObject({ allowedRoot: "candidate", members: 3, }); expect(JSON.parse(result.stdout).maxCachedMembers).toBeLessThanOrEqual(1); expect(existsSync(path.join(destination, "manifest.json"))).toBe(true); expect(existsSync(path.join(destination, "candidate", "payload.bin"))).toBe(true); expect(statSync(destination).mode & 0o777).toBe(0o700); }); it.runIf(hasGnuTar)( "accepts the producer's depth-first order around punctuation siblings", () => { const root = makeTempDir(); const archive = makeDepthFirstProducerArchive(root); const listing = spawnSync("bash", ["-c", 'zstd -dc "$1" | tar -tf -', "bash", archive], { encoding: "utf8", }); expect(listing.status, listing.stderr).toBe(0); expect(listing.stdout.trim().split("\n")).toEqual([ "manifest.json", "candidate/", "candidate/app/", "candidate/app/child", "candidate/app-routes.ts", ]); const destination = path.join(root, "depth-first-producer-output"); const result = expectSuccess([ "extract-zstd", archive, destination, "--allowed-root", "candidate", ]); expect(JSON.parse(result.stdout)).toMatchObject({ members: 5 }); expect(readFileSync(path.join(destination, "candidate", "app-routes.ts"), "utf8")).toBe( "routes\n", ); expect(readFileSync(path.join(destination, "candidate", "app", "child"), "utf8")).toBe( "child\n", ); }, ); it("rejects a member whose parent directory was not declared first", () => { const root = makeTempDir(); const tarPath = path.join(root, "missing-parent.tar"); const python = String.raw` import io import sys import tarfile with tarfile.open(sys.argv[1], "w", format=tarfile.USTAR_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) child = tarfile.TarInfo("candidate/missing/child.txt") child.size = 2 archive.addfile(child, io.BytesIO(b"ok")) `; const result = spawnSync("python3", ["-c", python, tarPath], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); const archive = compressTar(tarPath); const destination = path.join(root, "missing-parent-output"); expectFailure( ["extract-zstd", archive, destination, "--allowed-root", "candidate"], "archive member parent is not a prior directory", ); expect(existsSync(destination)).toBe(false); }); it("rejects compressed archives over the expanded-size cap and cleans up", () => { const root = makeTempDir(); const archive = makeCompressedArchive(root, 4096); const destination = path.join(root, "expanded-limit"); expectFailure( [ "extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-expanded-bytes", "1024", "--max-stream-bytes", `${1024 * 1024}`, ], "expanded size exceeds", ); expect(existsSync(destination)).toBe(false); }); it("extracts a prior-target hard link without retaining TarInfo records", () => { const root = makeTempDir(); const archive = makeValidHardlinkArchive(root); const destination = path.join(root, "hardlink-success"); const result = expectSuccess([ "extract-zstd", archive, destination, "--allowed-root", "candidate", ]); expect(JSON.parse(result.stdout).maxCachedMembers).toBeLessThanOrEqual(1); const target = path.join(destination, "candidate", "a-target.txt"); const link = path.join(destination, "candidate", "b-link.txt"); expect(readFileSync(link, "utf8")).toBe("shared\n"); expect(statSync(link).ino).toBe(statSync(target).ino); }); it("rejects compressed archives over the member-count cap and cleans up", () => { const root = makeTempDir(); const archive = compressTar(makeManyMemberTar(root, 3)); const destination = path.join(root, "member-limit"); expectFailure( ["extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-members", "2"], "member count exceeds 2", ); expect(existsSync(destination)).toBe(false); }); it.each(["pax", "gnu-longname", "gnu-longlink"] as const)( "rejects a declared %s extension before reading its payload", (kind) => { const root = makeTempDir(); const archive = makeDeclaredExtensionArchive(root, kind, 4096); const destination = path.join(root, `${kind}-limit`); expectFailure( [ "extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-extension-bytes", "1024", ], "extension payload exceeds 1024 bytes", ); expect(existsSync(destination)).toBe(false); }, ); it("rejects a global PAX header before reading its payload", () => { const root = makeTempDir(); const archive = makeDeclaredExtensionArchive(root, "pax-global", 4096); const destination = path.join(root, "pax-global-limit"); expectFailure( ["extract-zstd", archive, destination, "--allowed-root", "candidate"], "unsupported global PAX header", ); expect(existsSync(destination)).toBe(false); }); it("rejects archives over the cumulative extension payload cap", () => { const root = makeTempDir(); const archive = makeCumulativePaxArchive(root); const destination = path.join(root, "extension-total-limit"); expectFailure( [ "extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-extension-bytes", "1024", "--max-extension-total-bytes", "2048", ], "extension payload total exceeds 2048 bytes", ); expect(existsSync(destination)).toBe(false); }); it.each(["pax-path", "gnu-longname", "symlink", "hardlink"] as const)( "rejects an overlong %s path value", (kind) => { const root = makeTempDir(); const archive = makeLongMetadataArchive(root, kind); const destination = path.join(root, `${kind}-path-limit`); expectFailure( ["extract-zstd", archive, destination, "--allowed-root", "candidate"], "exceeds 4096 bytes", ); expect(existsSync(destination)).toBe(false); }, ); it("rejects archives over the aggregate path metadata cap", () => { const root = makeTempDir(); const archive = makeCompressedArchive(root); const destination = path.join(root, "path-limit"); expectFailure( [ "extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-path-bytes", "10", ], "path metadata exceeds 10 bytes", ); expect(existsSync(destination)).toBe(false); }); it("rejects a hard link from the candidate tree to the manifest", () => { const root = makeTempDir(); const source = path.join(root, "source-hardlink"); const candidate = path.join(source, "candidate"); mkdirSync(candidate, { recursive: true }); const manifest = path.join(source, "manifest.json"); writeFileSync(manifest, '{"version":1}\n'); linkSync(manifest, path.join(candidate, "manifest-copy.json")); const tarPath = path.join(root, "hardlink.tar"); const archivePath = `${tarPath}.zst`; const tarResult = spawnSync( "tar", ["-cf", tarPath, "-C", source, "manifest.json", "candidate"], { encoding: "utf8", env: { ...process.env, COPYFILE_DISABLE: "1" }, }, ); expect(tarResult.status, tarResult.stderr).toBe(0); const zstdResult = spawnSync("zstd", ["-q", "-f", tarPath, "-o", archivePath], { encoding: "utf8", }); expect(zstdResult.status, zstdResult.stderr).toBe(0); expectFailure( [ "extract-zstd", archivePath, path.join(root, "hardlink-output"), "--allowed-root", "candidate", ], "hard link target leaves candidate root", ); }); it("rejects a link that replaces a previously extracted descendant directory", () => { const root = makeTempDir(); const tarPath = path.join(root, "link-prefix.tar"); const python = String.raw` import io import sys import tarfile with tarfile.open(sys.argv[1], "w", format=tarfile.USTAR_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) prefix = tarfile.TarInfo("candidate/prefix") prefix.type = tarfile.DIRTYPE archive.addfile(prefix) payload = tarfile.TarInfo("candidate/prefix/file.txt") payload.size = 2 archive.addfile(payload, io.BytesIO(b"ok")) replacement = tarfile.TarInfo("candidate/prefix") replacement.type = tarfile.SYMTYPE replacement.linkname = "file.txt" archive.addfile(replacement) `; const result = spawnSync("python3", ["-c", python, tarPath], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); const archive = compressTar(tarPath); expectFailure( [ "extract-zstd", archive, path.join(root, "link-prefix-output"), "--allowed-root", "candidate", ], "archive has duplicate path", ); }); it("rejects duplicate canonical member paths", () => { const root = makeTempDir(); const tarPath = path.join(root, "duplicate.tar"); const python = String.raw` import io import sys import tarfile with tarfile.open(sys.argv[1], "w", format=tarfile.USTAR_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) for _ in range(2): duplicate = tarfile.TarInfo("candidate/duplicate") archive.addfile(duplicate) `; const result = spawnSync("python3", ["-c", python, tarPath], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); const archive = compressTar(tarPath); expectFailure( ["extract-zstd", archive, path.join(root, "duplicate-output"), "--allowed-root", "candidate"], "archive has duplicate path", ); }); it("rejects a member nested under a prior link", () => { const root = makeTempDir(); const tarPath = path.join(root, "link-parent.tar"); const python = String.raw` import io import sys import tarfile with tarfile.open(sys.argv[1], "w", format=tarfile.USTAR_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) link = tarfile.TarInfo("candidate/link") link.type = tarfile.SYMTYPE link.linkname = "target" archive.addfile(link) child = tarfile.TarInfo("candidate/link/child.txt") child.size = 2 archive.addfile(child, io.BytesIO(b"ok")) `; const result = spawnSync("python3", ["-c", python, tarPath], { encoding: "utf8", }); expect(result.status, result.stderr).toBe(0); const archive = compressTar(tarPath); expectFailure( [ "extract-zstd", archive, path.join(root, "link-parent-output"), "--allowed-root", "candidate", ], "path traverses a non-directory member", ); }); it("accepts unique 256-component paths within the metadata budget", () => { const root = makeTempDir(); const archive = makeDeepSortedArchive(root, 32); const destination = path.join(root, "deep-sorted-output"); const result = expectSuccess([ "extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-members", "10000", "--max-path-bytes", `${8 * 1024 * 1024}`, ]); expect(JSON.parse(result.stdout)).toMatchObject({ members: 8_162 }); expect(JSON.parse(result.stdout).maxCachedMembers).toBeLessThanOrEqual(1); }); it("bounds exact names and clears the stdlib TarInfo cache", () => { const source = readFileSync(SCRIPT, "utf8"); expect(source).toContain("seen_names"); expect(source).toContain("_validate_destination_paths"); expect(source).toContain(".lstat()"); expect(source).toContain("parent_name not in seen_names"); expect(source).toContain("_next_stream_member"); expect(source).toContain("archive.members.clear()"); expect(source).not.toContain("target_path.parent.mkdir"); expect(source).not.toContain("for member in archive"); expect(source).not.toContain("previous_sort_key"); expect(source).not.toContain("link_names"); expect(source).not.toContain("seen_parent_names"); expect(source).not.toContain("any(existing.startswith"); }); it("keeps TarInfo cache and resident memory bounded across 100000 members", () => { const root = makeTempDir(); const tarPath = makeManyMemberTar(root, 100_000); const result = probeTarInfoCache(tarPath); expect(result.status, result.stderr).toBe(0); const probe = JSON.parse(result.stdout); expect(probe).toMatchObject({ count: 100_000, }); expect(probe.maxCachedMembers).toBeLessThanOrEqual(1); expect(probe.residentKiB).toBeLessThan(128 * 1024); }, 30_000); it("clears PAX metadata from the TarInfo cache after every member", () => { const root = makeTempDir(); const archive = makePaxHeavyTar(root, 1_200, 128); const result = expectSuccess([ "extract-zstd", archive, path.join(root, "pax-heavy-output"), "--allowed-root", "candidate", "--max-members", "2000", ]); const summary = JSON.parse(result.stdout); expect(summary).toMatchObject({ members: 1_202 }); expect(summary.maxCachedMembers).toBeLessThanOrEqual(1); }, 30_000); it("rejects sparse archive members before extraction", () => { const root = makeTempDir(); const tarPath = path.join(root, "sparse.tar"); const archivePath = `${tarPath}.zst`; const python = String.raw` import io import sys import tarfile with tarfile.open(sys.argv[1], "w", format=tarfile.PAX_FORMAT) as archive: manifest = tarfile.TarInfo("manifest.json") manifest_payload = b'{"version":1}\n' manifest.size = len(manifest_payload) archive.addfile(manifest, io.BytesIO(manifest_payload)) root = tarfile.TarInfo("candidate") root.type = tarfile.DIRTYPE archive.addfile(root) sparse = tarfile.TarInfo("candidate/sparse.bin") sparse.size = 1 sparse.pax_headers = { "GNU.sparse.map": "0,1", "GNU.sparse.realsize": "2097152", } archive.addfile(sparse, io.BytesIO(b"x")) `; const tarResult = spawnSync("python3", ["-c", python, tarPath], { encoding: "utf8", }); expect(tarResult.status, tarResult.stderr).toBe(0); const zstdResult = spawnSync("zstd", ["-q", "-f", tarPath, "-o", archivePath], { encoding: "utf8", }); expect(zstdResult.status, zstdResult.stderr).toBe(0); expectFailure( [ "extract-zstd", archivePath, path.join(root, "sparse-output"), "--allowed-root", "candidate", ], "unsupported sparse member", ); }); it("rejects compressed archives over the stream cap and cleans up", () => { const root = makeTempDir(); const archive = makeCompressedArchive(root); const destination = path.join(root, "stream-limit"); expectFailure( [ "extract-zstd", archive, destination, "--allowed-root", "candidate", "--max-expanded-bytes", `${1024 * 1024}`, "--max-stream-bytes", "1024", ], "decompressed archive stream exceeds", ); expect(existsSync(destination)).toBe(false); }); it("rejects non-zero bytes after the tar end marker", () => { const root = makeTempDir(); const archive = makeCompressedArchive(root); const tarPath = archive.slice(0, -".zst".length); appendFileSync(tarPath, "EXFILTRATED-TRAILER"); const zstdResult = spawnSync("zstd", ["-q", "-f", tarPath, "-o", archive], { encoding: "utf8", }); expect(zstdResult.status, zstdResult.stderr).toBe(0); const destination = path.join(root, "trailing-output"); expectFailure( ["extract-zstd", archive, destination, "--allowed-root", "candidate"], "non-zero data after tar end", ); expect(existsSync(destination)).toBe(false); }); it("rejects a concatenated zstd frame after the tar payload", () => { const root = makeTempDir(); const archive = makeCompressedArchive(root); const trailerPath = path.join(root, "trailer.txt"); const trailerArchive = `${trailerPath}.zst`; writeFileSync(trailerPath, "EXFILTRATED-CONCATENATED-FRAME"); const zstdResult = spawnSync("zstd", ["-q", "-f", trailerPath, "-o", trailerArchive], { encoding: "utf8", }); expect(zstdResult.status, zstdResult.stderr).toBe(0); appendFileSync(archive, readFileSync(trailerArchive)); const destination = path.join(root, "concatenated-output"); expectFailure( ["extract-zstd", archive, destination, "--allowed-root", "candidate"], "non-zero data after tar end", ); expect(existsSync(destination)).toBe(false); }); });