diff --git a/scripts/e2e/lib/skills/clawhub-install-proof.sh b/scripts/e2e/lib/skills/clawhub-install-proof.sh index 4b2eff048541..56ece379867d 100644 --- a/scripts/e2e/lib/skills/clawhub-install-proof.sh +++ b/scripts/e2e/lib/skills/clawhub-install-proof.sh @@ -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)}`); } diff --git a/test/scripts/e2e-shell-tempfiles.test.ts b/test/scripts/e2e-shell-tempfiles.test.ts index 9678d9f05fbe..6ae4e816733b 100644 --- a/test/scripts/e2e-shell-tempfiles.test.ts +++ b/test/scripts/e2e-shell-tempfiles.test.ts @@ -21,6 +21,22 @@ async function listShellScripts(dir: string): Promise { return scripts; } +async function extractClawhubSkillInstallVerifier(): Promise { + 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 }); + } + }); });