Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions plugins/compound-engineering/skills/ce-code-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,10 @@ Then detect the review base branch and compute the merge-base. Run the bundled `
RESOLVE_SCRIPT=""
if [ -n "${CLAUDE_SKILL_DIR:-}" ] && [ -f "${CLAUDE_SKILL_DIR}/scripts/resolve-base.sh" ]; then
real_base=$(cd "${CLAUDE_SKILL_DIR}" 2>/dev/null && pwd -P) || real_base=""
real_cwd=$(pwd -P)
repo_top=$(git rev-parse --show-toplevel 2>/dev/null) || repo_top=""
if [ -n "$repo_top" ]; then real_repo=$(cd "$repo_top" 2>/dev/null && pwd -P) || real_repo=""; else real_repo=$(pwd -P); fi
case "$real_base" in
""|"$real_cwd"|"$real_cwd"/*) ;;
""|"$real_repo"|"$real_repo"/*) ;;
*) RESOLVE_SCRIPT="$real_base/scripts/resolve-base.sh" ;;
esac
fi
Expand Down Expand Up @@ -328,9 +329,10 @@ Detect the review base branch and compute the merge-base using the same bundled
RESOLVE_SCRIPT=""
if [ -n "${CLAUDE_SKILL_DIR:-}" ] && [ -f "${CLAUDE_SKILL_DIR}/scripts/resolve-base.sh" ]; then
real_base=$(cd "${CLAUDE_SKILL_DIR}" 2>/dev/null && pwd -P) || real_base=""
real_cwd=$(pwd -P)
repo_top=$(git rev-parse --show-toplevel 2>/dev/null) || repo_top=""
if [ -n "$repo_top" ]; then real_repo=$(cd "$repo_top" 2>/dev/null && pwd -P) || real_repo=""; else real_repo=$(pwd -P); fi
case "$real_base" in
""|"$real_cwd"|"$real_cwd"/*) ;;
""|"$real_repo"|"$real_repo"/*) ;;
*) RESOLVE_SCRIPT="$real_base/scripts/resolve-base.sh" ;;
esac
fi
Expand Down
30 changes: 30 additions & 0 deletions tests/review-skill-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,10 @@ describe("ce-code-review contract", () => {
// the base is unresolved.
expect(content).toContain('"${CLAUDE_SKILL_DIR:-}"')
expect(content).toContain('[ -f "${CLAUDE_SKILL_DIR}/scripts/resolve-base.sh" ]')
// Reject helper paths against the reviewed-repo top-level, not just CWD --
// otherwise a launch from a monorepo subdirectory leaves repo-controlled
// sibling paths (e.g., `<repo-root>/.evil`) accepted.
expect(content).toContain("git rev-parse --show-toplevel")
// No bare relative-path fallback to `scripts/resolve-base.sh` -- that would resolve
// against the reviewed repo's CWD and could execute a repo-controlled script.
expect(content).not.toContain('RESOLVE_SCRIPT="scripts/resolve-base.sh"')
Expand Down Expand Up @@ -750,6 +754,32 @@ describe("ce-code-review contract", () => {
expect(evil.status).not.toBe(0)
expect(evil.output).toContain("Re-run with `base:<ref>`")
expect(evil.output).not.toContain("from-cwd-evil")

// Monorepo subdir attack: reviewer launched from a subdirectory of the reviewed
// repo. CLAUDE_SKILL_DIR points at a sibling of the subdir (e.g., `<repo-root>/.evil`)
// -- it lives outside the CWD subdir but is still repo-controlled. The guard must
// reject it by comparing against the repo top-level, not just the CWD.
const monorepoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ce-code-review-monorepo-"))
try {
const subDir = path.join(monorepoRoot, "services", "foo")
const sneakSkill = path.join(monorepoRoot, ".evil")
fs.mkdirSync(subDir, { recursive: true })
fs.mkdirSync(path.join(sneakSkill, "scripts"), { recursive: true })
fs.writeFileSync(
path.join(sneakSkill, "scripts", "resolve-base.sh"),
"echo BASE:from-monorepo-evil\n",
)
execSync("git init -q", { cwd: monorepoRoot })

const realSubDir = fs.realpathSync(subDir)
const realSneakSkill = fs.realpathSync(sneakSkill)
const monorepo = runResolveProbe(snippet, realSubDir, { CLAUDE_SKILL_DIR: realSneakSkill })
expect(monorepo.status).not.toBe(0)
expect(monorepo.output).toContain("Re-run with `base:<ref>`")
expect(monorepo.output).not.toContain("from-monorepo-evil")
} finally {
fs.rmSync(monorepoRoot, { recursive: true, force: true })
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true })
}
Expand Down