Files
claude-engineering-plugin/tests/manifest-path-safety.test.ts
Trevin Chow c2d60b47be
Some checks failed
CI / pr-title (push) Has been cancelled
CI / test (push) Has been cancelled
Release PR / release-pr (push) Has been cancelled
Release PR / publish-cli (push) Has been cancelled
refactor(install): prefer native plugin install across targets (#609)
Co-authored-by: John Cavanaugh <cavanaug@users.noreply.github.com>
2026-04-20 18:47:07 -07:00

424 lines
15 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import fs from "fs/promises"
import os from "os"
import path from "path"
import { isSafeManagedPath } from "../src/utils/files"
import {
readManagedInstallManifest,
writeManagedInstallManifest,
cleanupRemovedManagedDirectories,
cleanupRemovedManagedFiles,
} from "../src/targets/managed-artifacts"
import { readCodexInstallManifest } from "../src/targets/codex"
import {
cleanupRemovedPiExtensions,
cleanupRemovedPiPrompts,
cleanupRemovedPiSkills,
readPiInstallManifest,
} from "../src/targets/pi"
describe("isSafeManagedPath", () => {
const root = "/tmp/managed-root"
test("accepts simple relative names", () => {
expect(isSafeManagedPath(root, "skill-name")).toBe(true)
expect(isSafeManagedPath(root, "foo.md")).toBe(true)
expect(isSafeManagedPath(root, "foo/bar")).toBe(true)
expect(isSafeManagedPath(root, "foo/bar/baz.toml")).toBe(true)
})
test("rejects non-string values", () => {
expect(isSafeManagedPath(root, undefined as unknown)).toBe(false)
expect(isSafeManagedPath(root, null as unknown)).toBe(false)
expect(isSafeManagedPath(root, 42 as unknown)).toBe(false)
expect(isSafeManagedPath(root, {} as unknown)).toBe(false)
})
test("rejects empty strings", () => {
expect(isSafeManagedPath(root, "")).toBe(false)
})
test("rejects absolute POSIX paths", () => {
expect(isSafeManagedPath(root, "/etc/passwd")).toBe(false)
expect(isSafeManagedPath(root, "/tmp/anything")).toBe(false)
})
test("rejects path traversal segments", () => {
expect(isSafeManagedPath(root, "..")).toBe(false)
expect(isSafeManagedPath(root, "../escape")).toBe(false)
expect(isSafeManagedPath(root, "../../../etc/passwd")).toBe(false)
expect(isSafeManagedPath(root, "foo/../bar")).toBe(false)
expect(isSafeManagedPath(root, "foo/../../escape")).toBe(false)
})
test("rejects windows-style absolute paths", () => {
// path.isAbsolute recognizes drive letters on win32 only; on posix
// the backslash form is treated as a literal filename, but the
// traversal split catches mixed separators.
expect(isSafeManagedPath(root, "..\\escape")).toBe(false)
expect(isSafeManagedPath(root, "foo\\..\\..\\escape")).toBe(false)
})
test("rejects entries that resolve outside root", () => {
// Even without `..` segments, the final containment check catches
// anything that would resolve outside the root.
expect(isSafeManagedPath(root, "..")).toBe(false)
})
})
describe("readManagedInstallManifest filters unsafe entries", () => {
let tempRoot: string
beforeEach(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "managed-manifest-"))
})
afterEach(async () => {
await fs.rm(tempRoot, { recursive: true, force: true })
})
test("drops traversal and absolute entries, keeps safe ones", async () => {
const managedDir = path.join(tempRoot, "managed")
await fs.mkdir(managedDir, { recursive: true })
const manifest = {
version: 1,
pluginName: "compound-engineering",
groups: {
skills: [
"safe-skill",
"../../../etc/passwd",
"/etc/passwd",
"foo/../bar",
"foo/../../escape",
"another-safe",
],
commands: ["ok.md"],
},
}
await fs.writeFile(path.join(managedDir, "install-manifest.json"), JSON.stringify(manifest))
const result = await readManagedInstallManifest(managedDir, "compound-engineering")
expect(result).not.toBeNull()
expect(result!.groups.skills).toEqual(["safe-skill", "another-safe"])
expect(result!.groups.commands).toEqual(["ok.md"])
})
test("returns null for wrong pluginName", async () => {
const managedDir = path.join(tempRoot, "managed")
await fs.mkdir(managedDir, { recursive: true })
const manifest = {
version: 1,
pluginName: "other-plugin",
groups: { skills: ["safe"] },
}
await fs.writeFile(path.join(managedDir, "install-manifest.json"), JSON.stringify(manifest))
const result = await readManagedInstallManifest(managedDir, "compound-engineering")
expect(result).toBeNull()
})
})
describe("cleanupRemovedManagedFiles does not escape root (defense in depth)", () => {
let tempRoot: string
beforeEach(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "managed-cleanup-"))
})
afterEach(async () => {
await fs.rm(tempRoot, { recursive: true, force: true })
})
test("skips unsafe entries even when fed directly (bypass read-time filter)", async () => {
const rootDir = path.join(tempRoot, "root")
await fs.mkdir(rootDir, { recursive: true })
const outsideFile = path.join(tempRoot, "outside.txt")
await fs.writeFile(outsideFile, "keep me")
// Simulate a manifest object assembled without going through
// readManagedInstallManifest's filter.
const hostileManifest = {
version: 1 as const,
pluginName: "compound-engineering",
groups: {
prompts: ["../outside.txt", "/etc/passwd"],
},
}
await cleanupRemovedManagedFiles(rootDir, hostileManifest, "prompts", [])
expect(await fs.readFile(outsideFile, "utf8")).toBe("keep me")
})
test("skips unsafe directory entries", async () => {
const rootDir = path.join(tempRoot, "root")
await fs.mkdir(rootDir, { recursive: true })
const outsideDir = path.join(tempRoot, "outside")
await fs.mkdir(outsideDir)
await fs.writeFile(path.join(outsideDir, "file.txt"), "keep me")
const hostileManifest = {
version: 1 as const,
pluginName: "compound-engineering",
groups: {
skills: ["../outside"],
},
}
await cleanupRemovedManagedDirectories(rootDir, hostileManifest, "skills", [])
expect(await fs.readFile(path.join(outsideDir, "file.txt"), "utf8")).toBe("keep me")
})
test("still cleans up safe entries correctly", async () => {
const rootDir = path.join(tempRoot, "root")
await fs.mkdir(rootDir, { recursive: true })
const safeFile = path.join(rootDir, "safe-prompt.md")
await fs.writeFile(safeFile, "remove me")
await writeManagedInstallManifest(rootDir, {
version: 1,
pluginName: "compound-engineering",
groups: { prompts: ["safe-prompt.md"] },
})
const manifest = await readManagedInstallManifest(rootDir, "compound-engineering")
expect(manifest).not.toBeNull()
// Simulate a follow-up install where "safe-prompt.md" is no longer
// in the current bundle — cleanup should remove it.
await cleanupRemovedManagedFiles(rootDir, manifest, "prompts", [])
let exists = true
try {
await fs.stat(safeFile)
} catch {
exists = false
}
expect(exists).toBe(false)
})
})
describe("readCodexInstallManifest filters unsafe entries", () => {
let tempRoot: string
beforeEach(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codex-manifest-"))
})
afterEach(async () => {
await fs.rm(tempRoot, { recursive: true, force: true })
})
test("drops traversal/absolute entries from skills, prompts, agents", async () => {
const codexRoot = path.join(tempRoot, ".codex")
const pluginDir = path.join(codexRoot, "compound-engineering")
await fs.mkdir(pluginDir, { recursive: true })
const manifest = {
version: 1,
pluginName: "compound-engineering",
skills: ["safe-skill", "../../../etc/passwd", "/etc/passwd"],
prompts: ["ok.md", "../../evil.md", "foo/../../escape.md"],
agents: ["safe-agent.toml", "/tmp/abs.toml", "../escape.toml"],
}
await fs.writeFile(path.join(pluginDir, "install-manifest.json"), JSON.stringify(manifest))
const result = await readCodexInstallManifest(codexRoot, "compound-engineering")
expect(result).not.toBeNull()
expect(result!.skills).toEqual(["safe-skill"])
expect(result!.prompts).toEqual(["ok.md"])
expect(result!.agents).toEqual(["safe-agent.toml"])
})
test("keeps all entries when all are safe", async () => {
const codexRoot = path.join(tempRoot, ".codex")
const pluginDir = path.join(codexRoot, "compound-engineering")
await fs.mkdir(pluginDir, { recursive: true })
const manifest = {
version: 1,
pluginName: "compound-engineering",
skills: ["a", "b", "c"],
prompts: ["p.md"],
agents: ["agent.toml"],
}
await fs.writeFile(path.join(pluginDir, "install-manifest.json"), JSON.stringify(manifest))
const result = await readCodexInstallManifest(codexRoot, "compound-engineering")
expect(result).not.toBeNull()
expect(result!.skills).toEqual(["a", "b", "c"])
expect(result!.prompts).toEqual(["p.md"])
expect(result!.agents).toEqual(["agent.toml"])
})
})
describe("readPiInstallManifest filters unsafe entries", () => {
let tempRoot: string
beforeEach(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-manifest-"))
})
afterEach(async () => {
await fs.rm(tempRoot, { recursive: true, force: true })
})
test("drops traversal/absolute entries from skills, prompts, extensions", async () => {
const piRoot = path.join(tempRoot, ".pi")
const managedDir = path.join(piRoot, "compound-engineering")
await fs.mkdir(managedDir, { recursive: true })
const paths = {
managedDir,
skillsDir: path.join(piRoot, "skills"),
promptsDir: path.join(piRoot, "prompts"),
extensionsDir: path.join(piRoot, "extensions"),
mcporterConfigPath: path.join(managedDir, "mcporter.json"),
agentsPath: path.join(piRoot, "AGENTS.md"),
}
const manifest = {
version: 1,
pluginName: "compound-engineering",
skills: ["safe-skill", "../../../etc/passwd", "/etc/passwd", "foo/../../escape"],
prompts: ["ok.md", "../../evil.md", "foo/../bar.md"],
extensions: ["safe.ext", "/tmp/abs.ext", "..\\escape.ext"],
}
await fs.writeFile(path.join(managedDir, "install-manifest.json"), JSON.stringify(manifest))
const result = await readPiInstallManifest(managedDir, "compound-engineering", paths)
expect(result).not.toBeNull()
expect(result!.skills).toEqual(["safe-skill"])
expect(result!.prompts).toEqual(["ok.md"])
expect(result!.extensions).toEqual(["safe.ext"])
})
test("keeps all entries when all are safe", async () => {
const piRoot = path.join(tempRoot, ".pi")
const managedDir = path.join(piRoot, "compound-engineering")
await fs.mkdir(managedDir, { recursive: true })
const paths = {
managedDir,
skillsDir: path.join(piRoot, "skills"),
promptsDir: path.join(piRoot, "prompts"),
extensionsDir: path.join(piRoot, "extensions"),
mcporterConfigPath: path.join(managedDir, "mcporter.json"),
agentsPath: path.join(piRoot, "AGENTS.md"),
}
const manifest = {
version: 1,
pluginName: "compound-engineering",
skills: ["a", "b", "c"],
prompts: ["p.md"],
extensions: ["ext.js"],
}
await fs.writeFile(path.join(managedDir, "install-manifest.json"), JSON.stringify(manifest))
const result = await readPiInstallManifest(managedDir, "compound-engineering", paths)
expect(result).not.toBeNull()
expect(result!.skills).toEqual(["a", "b", "c"])
expect(result!.prompts).toEqual(["p.md"])
expect(result!.extensions).toEqual(["ext.js"])
})
})
describe("Pi cleanup helpers do not escape root (defense in depth)", () => {
let tempRoot: string
beforeEach(async () => {
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "pi-cleanup-"))
})
afterEach(async () => {
await fs.rm(tempRoot, { recursive: true, force: true })
})
test("cleanupRemovedPiSkills skips unsafe entries fed directly", async () => {
const skillsDir = path.join(tempRoot, "skills")
await fs.mkdir(skillsDir, { recursive: true })
const outsideDir = path.join(tempRoot, "outside-skill")
await fs.mkdir(outsideDir)
await fs.writeFile(path.join(outsideDir, "file.txt"), "keep me")
const hostileManifest = {
version: 1 as const,
pluginName: "compound-engineering",
skills: ["../outside-skill", "/etc/passwd"],
prompts: [],
extensions: [],
}
await cleanupRemovedPiSkills(skillsDir, hostileManifest, [])
expect(await fs.readFile(path.join(outsideDir, "file.txt"), "utf8")).toBe("keep me")
})
test("cleanupRemovedPiPrompts skips unsafe entries fed directly", async () => {
const promptsDir = path.join(tempRoot, "prompts")
await fs.mkdir(promptsDir, { recursive: true })
const outsideFile = path.join(tempRoot, "outside.txt")
await fs.writeFile(outsideFile, "keep me")
const hostileManifest = {
version: 1 as const,
pluginName: "compound-engineering",
skills: [],
prompts: ["../outside.txt", "/etc/passwd"],
extensions: [],
}
await cleanupRemovedPiPrompts(promptsDir, hostileManifest, [])
expect(await fs.readFile(outsideFile, "utf8")).toBe("keep me")
})
test("cleanupRemovedPiExtensions skips unsafe entries fed directly", async () => {
const extensionsDir = path.join(tempRoot, "extensions")
await fs.mkdir(extensionsDir, { recursive: true })
const outsideFile = path.join(tempRoot, "outside-ext")
await fs.writeFile(outsideFile, "keep me")
const hostileManifest = {
version: 1 as const,
pluginName: "compound-engineering",
skills: [],
prompts: [],
extensions: ["../outside-ext", "/etc/passwd", "foo/../../escape"],
}
await cleanupRemovedPiExtensions(extensionsDir, hostileManifest, [])
expect(await fs.readFile(outsideFile, "utf8")).toBe("keep me")
})
test("still cleans up safe entries correctly", async () => {
const skillsDir = path.join(tempRoot, "skills")
const promptsDir = path.join(tempRoot, "prompts")
const extensionsDir = path.join(tempRoot, "extensions")
await fs.mkdir(skillsDir, { recursive: true })
await fs.mkdir(promptsDir, { recursive: true })
await fs.mkdir(extensionsDir, { recursive: true })
const staleSkillDir = path.join(skillsDir, "stale-skill")
await fs.mkdir(staleSkillDir)
await fs.writeFile(path.join(staleSkillDir, "SKILL.md"), "old")
const stalePrompt = path.join(promptsDir, "stale.md")
await fs.writeFile(stalePrompt, "old")
const staleExt = path.join(extensionsDir, "stale.ext")
await fs.writeFile(staleExt, "old")
const manifest = {
version: 1 as const,
pluginName: "compound-engineering",
skills: ["stale-skill"],
prompts: ["stale.md"],
extensions: ["stale.ext"],
}
await cleanupRemovedPiSkills(skillsDir, manifest, [])
await cleanupRemovedPiPrompts(promptsDir, manifest, [])
await cleanupRemovedPiExtensions(extensionsDir, manifest, [])
for (const p of [staleSkillDir, stalePrompt, staleExt]) {
let exists = true
try {
await fs.stat(p)
} catch {
exists = false
}
expect(exists).toBe(false)
}
})
})