diff --git a/scripts/e2e/bun-global-install-smoke.sh b/scripts/e2e/bun-global-install-smoke.sh index da79863e201d..ab9cc5f290d1 100755 --- a/scripts/e2e/bun-global-install-smoke.sh +++ b/scripts/e2e/bun-global-install-smoke.sh @@ -29,6 +29,29 @@ run_with_timeout() { node scripts/e2e/lib/bun-global-install/assertions.mjs run-with-timeout "$timeout_ms" "$@" } +resolve_pack_tarball_path() { + local pack_json_file="$1" + local pack_dir="$2" + node -e ' +const fs = require("node:fs"); +const path = require("node:path"); +const raw = fs.readFileSync(process.argv[1], "utf8") || "[]"; +const parsed = JSON.parse(raw); +const last = Array.isArray(parsed) ? parsed.at(-1) : null; +const filename = typeof last?.filename === "string" ? last.filename.trim() : ""; +if ( + !filename.endsWith(".tgz") || + filename.includes("\0") || + filename !== path.basename(filename) || + filename !== path.win32.basename(filename) +) { + console.error(`ERROR: npm pack reported unsafe tarball filename ${JSON.stringify(filename)}`); + process.exit(1); +} +process.stdout.write(path.resolve(process.argv[2], filename)); +' "$pack_json_file" "$pack_dir" +} + restore_dist_from_image() { local image="$1" local backup_dir="" @@ -122,17 +145,7 @@ resolve_package_tgz() { echo "==> Pack OpenClaw tarball" npm pack --ignore-scripts --json --pack-destination "$PACK_DIR" >"$pack_json_file" - PACKAGE_TGZ="$( - node -e ' -const raw = require("node:fs").readFileSync(process.argv[1], "utf8") || "[]"; -const parsed = JSON.parse(raw); -const last = Array.isArray(parsed) ? parsed.at(-1) : null; -if (!last || typeof last.filename !== "string" || last.filename.length === 0) { - process.exit(1); -} -process.stdout.write(require("node:path").resolve(process.argv[2], last.filename)); -' "$pack_json_file" "$PACK_DIR" - )" + PACKAGE_TGZ="$(resolve_pack_tarball_path "$pack_json_file" "$PACK_DIR")" if [ -z "$PACKAGE_TGZ" ] || [ ! -f "$PACKAGE_TGZ" ]; then echo "missing packed OpenClaw tarball" >&2 exit 1 diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index c90b9d209423..29ac365127bb 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -126,6 +126,40 @@ read_pack_tarball_filename "$pack_json_file"`, ); } +function extractResolvePackTarballPath(): string { + const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8"); + const match = script.match(/(resolve_pack_tarball_path\(\) \{[\s\S]*?\n\})\n\nrestore_dist/u); + if (!match) { + throw new Error("resolve_pack_tarball_path helper was not found"); + } + return match[1]; +} + +function runResolvePackTarballPath(filename: string) { + return spawnSync( + "bash", + [ + "--noprofile", + "--norc", + "-c", + `${extractResolvePackTarballPath()} +pack_dir="$(mktemp -d)" +pack_json_file="$pack_dir/pack.json" +trap 'rm -rf "$pack_dir"' EXIT +printf '%s' "$PACK_JSON" >"$pack_json_file" +resolve_pack_tarball_path "$pack_json_file" "$pack_dir"`, + ], + { + encoding: "utf8", + env: { + HOME: "/tmp", + PACK_JSON: JSON.stringify([{ filename }]), + PATH: process.env.PATH ?? "", + }, + }, + ); +} + describe("test-install-sh-docker", () => { it("defaults ARM hosts to native arm64 while keeping x64 CI on amd64", () => { expect(runDefaultSmokePlatform({ CI: "true" }, "aarch64")).toBe("linux/arm64"); @@ -542,6 +576,41 @@ describe("bun global install smoke", () => { expect(script).not.toContain('\n rm -rf "$ROOT_DIR/dist"\n'); }); + it("keeps npm pack tarball paths inside the Bun smoke pack directory", () => { + const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8"); + + expect(script).toContain("resolve_pack_tarball_path()"); + expect(script).toContain( + 'PACKAGE_TGZ="$(resolve_pack_tarball_path "$pack_json_file" "$PACK_DIR")"', + ); + expect(script).toContain("filename !== path.basename(filename)"); + expect(script).toContain("filename !== path.win32.basename(filename)"); + expect(script).toContain("npm pack reported unsafe tarball filename"); + }); + + it("rejects path-like npm pack tarball filenames in Bun smoke metadata", () => { + const safeResult = runResolvePackTarballPath("openclaw-2026.6.17.tgz"); + + expect(safeResult.status).toBe(0); + expect(safeResult.stdout).toMatch(/\/openclaw-2026\.6\.17\.tgz$/u); + + const unsafeFilenames = [ + "../openclaw.tgz", + "nested/openclaw.tgz", + "nested\\openclaw.tgz", + "/tmp/openclaw.tgz", + "C:\\temp\\openclaw.tgz", + "openclaw.tar.gz", + ]; + + for (const filename of unsafeFilenames) { + const result = runResolvePackTarballPath(filename); + + expect(result.status, filename).not.toBe(0); + expect(result.stderr, filename).toContain("npm pack reported unsafe tarball filename"); + } + }); + it("gates workflow Bun install smoke to scheduled and release-check runs", () => { const workflow = readFileSync(INSTALL_SMOKE_WORKFLOW_PATH, "utf8"); const releaseChecks = readFileSync(RELEASE_CHECKS_WORKFLOW_PATH, "utf8");