Source: BLOCKING #1 from @scottschreckengaust's review of #665 — #665 (review) (agent/src/registry/loader.py:190)
Parent: #246 · Sibling blockers: the fail-open guard and the skip-worktree data-loss issues (linked below)
Problem
apply_mcp_assets computes mcp_path = os.path.join(repo_dir, ".mcp.json") and opens it "w". Python's open(..., "w") follows symlinks, so if the cloned repo ships .mcp.json -> config/mcp.json (both tracked — a normal shared-config layout), the resolved unredacted MCP runtime (bearer headers, url?token=, --api-key args) lands in config/mcp.json. The _protect_mcp_json_from_commit guard then flags only the symlink's index entry (.mcp.json), leaving config/mcp.json unguarded — so post_hooks.ensure_committed (git add -u) stages it and ensure_pushed pushes the secret into the PR's git history. Reproduced against the real loader at 0b06ff5.
The same symlink-follow is an arbitrary-file-write primitive: .mcp.json -> .github/workflows/ci.yml overwrites the workflow (and add -u stages it); .mcp.json -> .git/config corrupts repo config so every later git call fails. Repo layout is attacker-controlled input — the agent clones untrusted repos / PR branches (repo.py:299).
Fix
Refuse a non-regular target before writing:
if os.path.islink(mcp_path):
raise RegistryAssetLoadError(f"refusing to write resolved MCP config through a symlink: {mcp_path}")
(os.open(..., O_NOFOLLOW) is the race-free variant.)
Note: Scott's recommended durable fix — route registry MCP servers through the SDK in-process mcp_servers option instead of writing .mcp.json at all — closes this together with the sibling blockers. If that path is taken, this issue is subsumed.
Acceptance
- A symlinked
.mcp.json (pointing at a second tracked file, or into .git/.github) causes apply_mcp_assets to raise, not write-through
- Regression test: symlink
.mcp.json at a second tracked file, assert raise + git add -u && git diff --cached empty
Source: BLOCKING #1 from @scottschreckengaust's review of #665 — #665 (review) (
agent/src/registry/loader.py:190)Parent: #246 · Sibling blockers: the fail-open guard and the skip-worktree data-loss issues (linked below)
Problem
apply_mcp_assetscomputesmcp_path = os.path.join(repo_dir, ".mcp.json")and opens it"w". Python'sopen(..., "w")follows symlinks, so if the cloned repo ships.mcp.json -> config/mcp.json(both tracked — a normal shared-config layout), the resolved unredacted MCP runtime (bearer headers,url?token=,--api-keyargs) lands inconfig/mcp.json. The_protect_mcp_json_from_commitguard then flags only the symlink's index entry (.mcp.json), leavingconfig/mcp.jsonunguarded — sopost_hooks.ensure_committed(git add -u) stages it andensure_pushedpushes the secret into the PR's git history. Reproduced against the real loader at0b06ff5.The same symlink-follow is an arbitrary-file-write primitive:
.mcp.json -> .github/workflows/ci.ymloverwrites the workflow (andadd -ustages it);.mcp.json -> .git/configcorrupts repo config so every later git call fails. Repo layout is attacker-controlled input — the agent clones untrusted repos / PR branches (repo.py:299).Fix
Refuse a non-regular target before writing:
(
os.open(..., O_NOFOLLOW)is the race-free variant.)Acceptance
.mcp.json(pointing at a second tracked file, or into.git/.github) causesapply_mcp_assetsto raise, not write-through.mcp.jsonat a second tracked file, assert raise +git add -u && git diff --cachedempty