Skip to content

fix(mcp): contain policy_boundary_check scan roots to the source boundary (legis-0186c23a2c)#22

Open
tachyon-beep wants to merge 4 commits into
mainfrom
fix/policy-boundary-containment
Open

fix(mcp): contain policy_boundary_check scan roots to the source boundary (legis-0186c23a2c)#22
tachyon-beep wants to merge 4 commits into
mainfrom
fix/policy-boundary-containment

Conversation

@tachyon-beep

Copy link
Copy Markdown
Collaborator

Summary

Closes legis-0186c23a2c (Codex Security finding) — the last open north-star governance-honesty P2. The MCP policy_boundary_check tool accepted caller-supplied scan roots (root / repo_root) that were absolute or ..-bearing and outside the server's configured source boundary, then walked them — letting an MCP caller make Legis read/parse arbitrary Python trees visible to the server process (path/parse-result leakage, expensive traversal).

The fix

  • assert_within_boundary(anchor, *candidates) — a pure containment primitive in the policy truth layer (src/legis/policy/boundary_scan.py). It resolve()s each candidate (collapsing .. and symlinks) and raises InvalidArgumentError if any escapes the resolved anchor. Deferred service.errors import avoids a policy↔service cycle.
  • _tool_policy_boundary_check (src/legis/mcp.py) now calls it with anchor=source_root before any filesystem walk. An out-of-bounds root fails closed to an INVALID_ARGUMENT error envelope (isError: True) — never a result, never PASS/FINDINGS/NO_ROOT. The result outcome enum is unchanged.

Scope

  • MCP only. The CLI policy-boundary-check is operator-invoked with local filesystem authority (it is the CI self-scan gate); containment there adds no security and would break legitimate operator runs, so it is deliberately out of scope. No HTTP route exists.
  • A nonexistent-but-in-bounds root still resolves to NO_ROOT (unchanged) — only out-of-bounds roots error.
  • In-walk symlink traversal (a symlink discovered inside a contained tree) needs filesystem-write access and is outside this finding's caller-supplied-args threat model — explicitly noted, not silently skipped.

Verification

  • New tests: 6 unit tests for the primitive (allow child/anchor; reject sibling/../symlink-as-root; checks every candidate) + 5 MCP tests (3 reject vectors with fail-closed leak checks, 1 in-bounds-absolute regression guard, 1 discriminating repo_root-out/root-in case).
  • The repo_root containment is mutation-proven: dropping repo_root from the containment call makes the discriminating test fail (it scans instead of erroring).
  • Gates green off main: ruff, mypy (78 files), full suite 1275 passed / 9 skipped @ 92.04% coverage (floor 88), all 7 per-package floors, SEI conformance oracle, the governance self-scan (legis policy-boundary-check --root src --repo-root . → PASS), and governance-gate.

North-star impact: open confirmed governance-honesty defects 1 → 0.

🤖 Generated with Claude Code

tachyon-beep and others added 4 commits June 28, 2026 18:08
- Resolves candidate paths and fails closed (InvalidArgumentError) when any
  escapes the anchor; collapses .. and symlinks via resolve()
- Deferred service.errors import to avoid a policy<->service cycle
- Toward legis-0186c23a2c

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dary

Closes legis-0186c23a2c. An MCP caller could pass an absolute or ..-bearing
root/repo_root and make Legis walk/parse arbitrary Python trees visible to the
server process. The tool now calls assert_within_boundary(source_root, ...)
before any filesystem walk; an out-of-bounds root fails closed to an
INVALID_ARGUMENT error envelope (never a scanned result / PASS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The existing repo_root reject-test exercised containment only incidentally
(its default root, repo_root/src, was also out of bounds), so dropping
repo_root from assert_within_boundary would have left every MCP test green.
Add a discriminating case (repo_root out-of-bounds, root explicitly in-bounds)
+ the symmetric scanned_root-absence assert; mutation-proven to fail if
repo_root containment is removed. Final-review Important finding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan for legis-0186c23a2c executed in this branch (subagent-driven TDD).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant