fix(release): reject unsafe candidate pack names

This commit is contained in:
Vincent Koc
2026-06-17 03:19:03 +02:00
parent b338a68e57
commit 5b077d549e
2 changed files with 93 additions and 7 deletions

View File

@@ -141,6 +141,21 @@ function validateOutputName(value) {
}
}
function resolvePackedOpenClawTarballFilename(value) {
const filename = typeof value === "string" ? value.trim() : "";
if (
!/^openclaw-[A-Za-z0-9._-]+\.tgz$/u.test(filename) ||
filename.includes("\0") ||
filename !== path.basename(filename) ||
filename !== path.win32.basename(filename)
) {
throw new Error(
`npm pack reported unsafe OpenClaw tarball filename: ${JSON.stringify(filename)}`,
);
}
return filename;
}
export function validateOpenClawPackageSpec(spec) {
if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) {
throw new Error(
@@ -502,24 +517,41 @@ async function installPackageSourceDeps(sourceDir) {
async function moveNewestPackedTarball(outputDir, packOutput, outputName) {
let filename = "";
let parsed;
try {
const parsed = JSON.parse(packOutput);
if (Array.isArray(parsed)) {
filename = parsed.find((entry) => typeof entry?.filename === "string")?.filename ?? "";
}
parsed = JSON.parse(packOutput);
} catch {}
if (Array.isArray(parsed)) {
const packedFilename =
parsed.find((entry) => typeof entry?.filename === "string")?.filename ?? "";
if (packedFilename) {
filename = resolvePackedOpenClawTarballFilename(packedFilename);
}
}
if (!filename) {
for (const line of packOutput.split(/\r?\n/u)) {
const trimmed = line.trim();
if (/^openclaw-.*\.tgz$/u.test(trimmed)) {
filename = trimmed;
if (
trimmed.endsWith(".tgz") &&
(trimmed.startsWith("openclaw-") ||
trimmed.includes(":") ||
trimmed.includes("/") ||
trimmed.includes("\\"))
) {
filename = resolvePackedOpenClawTarballFilename(trimmed);
}
}
}
if (!filename) {
const entries = await fs.readdir(outputDir);
filename = entries
.filter((entry) => /^openclaw-.*\.tgz$/u.test(entry))
.filter((entry) => {
try {
return resolvePackedOpenClawTarballFilename(entry) === entry;
} catch {
return false;
}
})
.toSorted((a, b) => a.localeCompare(b))
.at(-1);
}
@@ -535,6 +567,8 @@ async function moveNewestPackedTarball(outputDir, packOutput, outputName) {
return target;
}
export const moveNewestPackedTarballForTest = moveNewestPackedTarball;
function normalizeUrlHostname(hostname) {
return hostname.replace(/^\[/u, "").replace(/\]$/u, "").replace(/\.+$/u, "").toLowerCase();
}

View File

@@ -12,6 +12,7 @@ import {
downloadUrl,
findSingleTarballForTest,
loadTrustedPackageSource,
moveNewestPackedTarballForTest,
parseArgs,
readArtifactPackageCandidateMetadata,
readPackageBuildSourceSha,
@@ -211,6 +212,57 @@ describe("resolve-openclaw-package-candidate", () => {
});
});
it("keeps npm pack filenames inside the package candidate output directory", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-npm-pack-"));
tempDirs.push(dir);
await writeFile(path.join(dir, "openclaw-2026.6.17.tgz"), "package");
await expect(
moveNewestPackedTarballForTest(
dir,
JSON.stringify([{ filename: "openclaw-2026.6.17.tgz" }]),
"openclaw-current.tgz",
),
).resolves.toBe(path.join(dir, "openclaw-current.tgz"));
await expect(readFile(path.join(dir, "openclaw-current.tgz"), "utf8")).resolves.toBe("package");
});
it("rejects path-like npm pack filenames instead of renaming outside the output directory", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-npm-pack-"));
tempDirs.push(dir);
const unsafeFilenames = [
"../openclaw-2026.6.17.tgz",
"nested/openclaw-2026.6.17.tgz",
"nested\\openclaw-2026.6.17.tgz",
"/tmp/openclaw-2026.6.17.tgz",
"C:\\temp\\openclaw-2026.6.17.tgz",
"openclaw-2026.6.17.tar.gz",
];
for (const filename of unsafeFilenames) {
await expect(
moveNewestPackedTarballForTest(dir, JSON.stringify([{ filename }]), "openclaw-current.tgz"),
).rejects.toThrow("npm pack reported unsafe OpenClaw tarball filename");
}
});
it("rejects unsafe text npm pack filenames instead of using loose stdout fallback", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-npm-pack-"));
tempDirs.push(dir);
await writeFile(path.join(dir, "openclaw-2026.6.17.tgz"), "safe fallback");
for (const filename of ["../openclaw-2026.6.17.tgz", "C:openclaw-2026.6.17.tgz"]) {
await expect(
moveNewestPackedTarballForTest(
dir,
["npm notice", filename].join("\n"),
"openclaw-current.tgz",
),
).rejects.toThrow("npm pack reported unsafe OpenClaw tarball filename");
}
});
it("bounds captured command stderr tails on failures", async () => {
await expect(
runCommandForTest(