fix(e2e): reject escaped skill info paths

This commit is contained in:
Vincent Koc
2026-06-20 08:31:42 +02:00
parent 075965e32f
commit 7b44157bc6
2 changed files with 73 additions and 1 deletions
@@ -125,6 +125,10 @@ import fs from "node:fs";
import path from "node:path";
const [configPath, skillDir, originPath, lockPath, infoPath, slug] = process.argv.slice(2);
const read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
function isPathInside(parentPath, childPath) {
const relative = path.relative(path.resolve(parentPath), path.resolve(childPath));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
const config = read(configPath);
if (config.skills?.install?.allowUploadedArchives !== false) {
throw new Error("skills.install.allowUploadedArchives must remain false during ClawHub install proof");
@@ -142,7 +146,7 @@ const infoFilePath = info.filePath ?? info.skill?.filePath;
const infoBaseDir = info.baseDir ?? info.skill?.baseDir;
if (
info.skillKey !== slug &&
(!infoFilePath || !path.resolve(infoFilePath).startsWith(path.resolve(skillDir)))
(!infoFilePath || !isPathInside(skillDir, infoFilePath))
) {
throw new Error(`skills info did not report installed skill ${slug}: ${JSON.stringify(info)}`);
}
+68
View File
@@ -21,6 +21,22 @@ async function listShellScripts(dir: string): Promise<string[]> {
return scripts;
}
async function extractClawhubSkillInstallVerifier(): Promise<string> {
const script = await readFile("scripts/e2e/lib/skills/clawhub-install-proof.sh", "utf8");
const marker =
'node --input-type=module - "$OPENCLAW_CONFIG_PATH" "$skill_dir" "$origin_json" "$lock_json" "$info_json" "$slug" <<\'NODE\'\n';
const start = script.indexOf(marker);
if (start === -1) {
throw new Error("ClawHub skill install verifier heredoc was not found");
}
const verifierStart = start + marker.length;
const verifierEnd = script.indexOf("\nNODE", verifierStart);
if (verifierEnd === -1) {
throw new Error("ClawHub skill install verifier heredoc was not terminated");
}
return script.slice(verifierStart, verifierEnd);
}
describe("e2e shell tempfile hygiene", () => {
it("does not allocate FIFO paths with mktemp -u", async () => {
const offenders: string[] = [];
@@ -254,4 +270,56 @@ exit 42
await rm(tempRoot, { force: true, recursive: true });
}
});
it("rejects ClawHub skill info paths that only share a resolved prefix", async () => {
const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-clawhub-info-path-"));
const workspaceDir = path.join(tempRoot, "workspace");
const slug = "demo";
const skillDir = path.join(workspaceDir, "skills", slug);
const escapedInfoPath = path.join(workspaceDir, "skills", `${slug}-escape`, "SKILL.md");
const configPath = path.join(tempRoot, "openclaw.json");
const originPath = path.join(skillDir, ".clawhub", "origin.json");
const lockPath = path.join(workspaceDir, ".clawhub", "lock.json");
const infoPath = path.join(tempRoot, "info.json");
try {
await mkdir(path.dirname(originPath), { recursive: true });
await mkdir(path.dirname(lockPath), { recursive: true });
await writeFile(path.join(skillDir, "SKILL.md"), `---\nname: Demo\n---\n`);
await writeFile(
configPath,
`${JSON.stringify({ skills: { install: { allowUploadedArchives: false } } })}\n`,
);
await writeFile(
originPath,
`${JSON.stringify({
installedVersion: "1.0.0",
registry: "https://clawhub.ai",
slug,
})}\n`,
);
await writeFile(
lockPath,
`${JSON.stringify({ skills: { [slug]: { version: "1.0.0" } } })}\n`,
);
await writeFile(
infoPath,
`${JSON.stringify({ filePath: escapedInfoPath, skillKey: "wrong-skill" })}\n`,
);
const result = spawnSync(
process.execPath,
["--input-type=module", "-", configPath, skillDir, originPath, lockPath, infoPath, slug],
{
encoding: "utf8",
input: await extractClawhubSkillInstallVerifier(),
},
);
expect(result.status).not.toBe(0);
expect(result.stderr).toContain("skills info did not report installed skill demo");
} finally {
await rm(tempRoot, { force: true, recursive: true });
}
});
});