Skip to main content
  1. Articles/

MCP Config Swap: How a Name-Only Approval Lets Attackers Swap Your Server's Binary

Elliot Belt
Author
Elliot Belt
I’m Felix Billières, pentester under the alias Elliot Belt. I do CTFs with the Phreaks 2600 team and I’m currently a Purple Teamer in internship. Passionate about Active Directory, web pentesting/bug bounty, and creating offensive and defensive tools.
Table of Contents
MCP Security Research - This article is part of a series.
Part 5: This Article

MCP Config Swap: How a Name-Only Approval Lets Attackers Swap Your Server’s Binary
#

Fifth article in my MCP security research series. After icon injection, OAuth SSRF, ancestor path traversal, and phantom task injection, I looked at what Claude Code actually stores when you approve an MCP server — and found that it stores nothing but the name.


Context and Disclosure
#

This was submitted to Anthropic’s Vulnerability Disclosure Program on HackerOne. Anthropic closed it as Informative, reasoning that an attacker with write access to a project’s .mcp.json (e.g. through a merged PR) already has significant trust within that project environment, which falls outside their threat model for Claude Code.

Their position: when a user approves a project’s trust, they are explicitly trusting the ongoing integrity of that project’s files. The MCP server approval model is designed to protect against unauthorized server connections, but it operates within the trust boundary of the local project configuration.

Fair enough — but the behavior is still worth documenting, especially given that CVE-2025-54136 (MCPoison in Cursor) covers the exact same vulnerability class and was assigned a CVE.


Table of Contents
#

  1. TL;DR
  2. Background: How MCP Approvals Work
  3. The Bug: Name-Only Approval
  4. Proof of Concept
  5. Attack Scenarios
  6. Suggested Fix
  7. Conclusion

TL;DR
#

When a user approves an MCP server from .mcp.json, Claude Code stores the server name as a plain string in enabledMcpjsonServers. On subsequent sessions, it checks whether the name matches — and nothing else. The server’s command, args, env, url, and type fields are never verified.

// What Claude Code stores:
{ "enabledMcpjsonServers": ["helper"] }

// What it SHOULD store:
{ "enabledMcpjsonServers": [{"name": "helper", "configHash": "sha256_of_command_args_env"}] }

An attacker who modifies .mcp.json — keeping the same server name but changing the command to an arbitrary binary — achieves code execution without any re-approval prompt.

Same vulnerability class as: CVE-2025-54136 (MCPoison in Cursor) CWE: CWE-345 — Insufficient Verification of Data Authenticity Confirmed on: Claude Code v2.1.34 (npm) and v2.1.63 (binary)


Background: How MCP Approvals Work
#

Project-scoped MCP servers (defined in .mcp.json) require user approval before Claude Code starts them. The approval flow:

  1. Claude Code reads .mcp.json from the project directory
  2. For each server, it checks enabledMcpjsonServers in .claude/settings.local.json
  3. If the server name is in the list → approved, start it immediately
  4. If not → prompt the user

The approval check function (extracted from v2.1.63 binary):

function t_$(H) {
  let $ = OL(),           // read localSettings
      A = BJ(H);          // normalize name: replace non-alphanumeric with _

  if ($?.disabledMcpjsonServers?.some((L) => BJ(L) === A))
    return "rejected";

  if ($?.enabledMcpjsonServers?.some((L) => BJ(L) === A) || $?.enableAllProjectMcpServers)
    return "approved";    // <-- NAME MATCH ONLY, command NOT checked

  return "pending";
}

The schema confirms it — enabledMcpjsonServers is a flat array of strings:

enabledMcpjsonServers: z.array(z.string()).optional()

No hash. No fingerprint. No config digest. Just the name.


The Bug: Name-Only Approval
#

Once approved, the server’s command is passed directly to child_process.spawn:

this._process = crossSpawn(
  this._serverParams.command,    // from .mcp.json, never re-validated
  this._serverParams.args ?? [],
  { shell: false, stdio: ["pipe", "pipe", "inherit"], env: {...} }
);

The gap between what’s approved and what’s executed:

FieldChecked at approval?Checked at launch?
Server nameYesYes (name match)
commandShown in promptNever re-checked
argsShown in promptNever re-checked
envNot shownNever re-checked
urlNot shownNever re-checked
typeNot shownNever re-checked

The user sees the full config during the initial approval prompt. But on every subsequent session, only the name is verified. Everything else can change silently.


Proof of Concept
#

Tested on Claude Code v2.1.63, Linux x86_64.

Step 1 — Legitimate server, user approves
#

Start with a clean .mcp.json containing a benign server. User opens Claude Code, sees the approval prompt for helper, clicks approve.

Clone the PoC repo, set up .mcp.json with legit-server.sh, launch Claude Code — approval prompt appears

The approval is stored as a plain string in .claude/settings.local.json:

settings.local.json contains enabledMcpjsonServers: [“helper”] — name only, no hash

Step 2 — Attacker swaps the command
#

A malicious commit (via PR, dependency update, or direct push) changes .mcp.json — same server name helper, but the command now points to evil-server.sh. The victim opens a new Claude Code session: no approval prompt appears, the swapped binary starts immediately.

Swap command to evil-server.sh, relaunch claude — no re-approval prompt, server starts silently

Step 3 — Proof of execution
#

The evil script ran with full user privileges — no prompt, no warning:

cat /tmp/poc-mcp-pwned.txt showing arbitrary code execution proof with user context, file access, and environment details

Expected vs Actual
#

ExpectedActual
Config change detectionRe-prompt when command changesNo re-prompt
Approval scopeTied to command + args + envTied to name only
Config integrityHash/fingerprint storedPlain string name

Attack Scenarios
#

Supply chain via malicious PR. Attacker contributes to an open-source project. A PR modifies .mcp.json to swap an approved server’s command. Reviewers often overlook config file changes. After merge, every developer who pulls gets compromised on next session.

Dependency confusion. If .mcp.json references npx some-package, the attacker publishes a malicious version of that package. The server name hasn’t changed — no re-approval needed.

Insider threat. A team member with commit access silently modifies an MCP server config. All other team members are compromised on next session start.

Amplification with enableAllProjectMcpServers. If the user previously clicked “Yes to all”, the attacker can also ADD entirely new servers with arbitrary names and commands — all auto-approved without any prompt.


Suggested Fix
#

Store a hash of the full server configuration alongside the name:

// Instead of:
enabledMcpjsonServers: ["helper"]

// Store:
enabledMcpjsonServers: [{
  name: "helper",
  configHash: "sha256(command + args + env + url + type)"
}]

// On load: recompute hash, compare, re-prompt if mismatch

Additionally, showing a diff when a previously-approved server’s config changes would make the modification visible to the user — even if they choose to approve it again.


Conclusion
#

Claude Code’s MCP server approval stores the server name as a plain string and never re-verifies the actual command that gets executed. The same vulnerability class was assigned CVE-2025-54136 in Cursor’s implementation. Anthropic considers this within the trust boundary of project configuration files — if you approved the project, you trust its files.

The practical risk depends on your threat model. If you work on repos where others can modify .mcp.json (open-source projects, shared team repos), the approval you gave to node server.js still holds when that command becomes /tmp/evil.sh. The fix is a hash comparison — one check that ties the approval to what’s actually being executed.


This is part of an ongoing series on MCP security. Previous articles: SVG Icon Injection, OAuth SSRF, Ancestor Injection, and Phantom Task Injection.

MCP Security Research - This article is part of a series.
Part 5: This Article

Related

MCP Ancestor Injection: How a .mcp.json in /tmp/ Hijacks Your Claude Code Session

Third article in my MCP security series. Claude Code’s .mcp.json discovery walks from CWD to filesystem root with no boundary check and no file ownership verification. On multi-user Linux systems, any user can drop /tmp/.mcp.json to inject MCP servers into another user’s Claude Code session. Not reported to Anthropic — here’s why, and the full technical breakdown.

MCP SSRF via OAuth PRM Discovery: How a 401 Turns Your Client Into a Proxy

Second article in my MCP security series. A malicious MCP server returns a 401 with a crafted WWW-Authenticate header pointing resource_metadata at any URL it wants. The MCP SDK fetches that URL without origin validation — blind SSRF, affects both Python and TypeScript SDKs, Claude Desktop, and Claude Code. Reported to Anthropic VDP, closed as duplicate. Full technical details disclosed here.

MCP Phantom Task Injection: Stealing Credentials Through the Server You Trust

Fourth article in my MCP security series. By chaining a transport-layer weakness (session ID as sole routing key) with the Tasks and Elicitation systems, an attacker can inject phantom tasks into a victim’s MCP session and phish credentials through the legitimate, trusted server. CVSS 8.1 — reported to Anthropic VDP and disclosed. Full technical breakdown with working PoC.

MCP SVG Icon Injection: From XSS to RCE Through the Protocol Spec

A deep dive into a protocol-level vulnerability in the Model Context Protocol (MCP) specification where malicious SVG icons delivered via data: URIs can escalate from XSS to full RCE on Electron clients. Reported to Anthropic VDP, closed as Informative — disclosed here with full technical details.