mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 14:58:09 +00:00
fix(release): reject unsafe candidate pack names
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user