diff --git a/scripts/package-openclaw-for-docker.mjs b/scripts/package-openclaw-for-docker.mjs index a896b9825ae..7a23a072517 100644 --- a/scripts/package-openclaw-for-docker.mjs +++ b/scripts/package-openclaw-for-docker.mjs @@ -78,6 +78,12 @@ function readEqualsOptionValue(value, optionName) { return value; } +function validateOutputName(value) { + if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.t(?:ar\.)?gz$/u.test(value)) { + throw new Error(`--output-name must be a tarball filename, not a path: ${value}`); + } +} + export function parseArgs(argv) { const options = { outputDir: "", @@ -111,6 +117,9 @@ export function parseArgs(argv) { throw new Error(`unknown argument: ${arg}`); } } + if (options.outputName) { + validateOutputName(options.outputName); + } return options; } diff --git a/scripts/resolve-openclaw-package-candidate.mjs b/scripts/resolve-openclaw-package-candidate.mjs index dc8c41197cb..210b69aa39a 100644 --- a/scripts/resolve-openclaw-package-candidate.mjs +++ b/scripts/resolve-openclaw-package-candidate.mjs @@ -131,9 +131,16 @@ export function parseArgs(argv) { throw new Error(`unknown argument: ${arg}`); } } + validateOutputName(options.outputName); return options; } +function validateOutputName(value) { + if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.t(?:ar\.)?gz$/u.test(value)) { + throw new Error(`--output-name must be a tarball filename, not a path: ${value}`); + } +} + export function validateOpenClawPackageSpec(spec) { if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) { throw new Error( diff --git a/test/scripts/package-openclaw-for-docker.test.ts b/test/scripts/package-openclaw-for-docker.test.ts index a286b0ae660..bec7015c84d 100644 --- a/test/scripts/package-openclaw-for-docker.test.ts +++ b/test/scripts/package-openclaw-for-docker.test.ts @@ -96,6 +96,23 @@ describe("package-openclaw-for-docker", () => { } }); + it("rejects package artifact output names that escape the output directory", () => { + for (const outputName of [ + "../openclaw-current.tgz", + "nested/openclaw-current.tgz", + "openclaw-current.zip", + ".openclaw-current.tgz", + ]) { + expect(() => parseArgs(["--output-name", outputName])).toThrow( + `--output-name must be a tarball filename, not a path: ${outputName}`, + ); + } + + expect(parseArgs(["--output-name", "openclaw-current.tar.gz"]).outputName).toBe( + "openclaw-current.tar.gz", + ); + }); + it("uses build-all as the single bounded package artifact build step", async () => { const calls: Array<{ command: string; diff --git a/test/scripts/resolve-openclaw-package-candidate.test.ts b/test/scripts/resolve-openclaw-package-candidate.test.ts index fc7e585f731..9df63568713 100644 --- a/test/scripts/resolve-openclaw-package-candidate.test.ts +++ b/test/scripts/resolve-openclaw-package-candidate.test.ts @@ -165,6 +165,23 @@ describe("resolve-openclaw-package-candidate", () => { }); }); + it("rejects package candidate output names that escape the output directory", () => { + for (const outputName of [ + "../openclaw-current.tgz", + "nested/openclaw-current.tgz", + "openclaw-current.zip", + ".openclaw-current.tgz", + ]) { + expect(() => parseArgs(["--output-name", outputName])).toThrow( + `--output-name must be a tarball filename, not a path: ${outputName}`, + ); + } + + expect(parseArgs(["--output-name", "openclaw-current.tar.gz"]).outputName).toBe( + "openclaw-current.tar.gz", + ); + }); + it("resolves npm package candidates through the Windows npm.cmd toolchain shim", () => { const execPath = "C:\\nodejs\\node.exe"; const npmCmdPath = path.win32.resolve(path.win32.dirname(execPath), "npm.cmd");