IV.3IV · The protocol layerattack
The MCP assessment runbook
IV.2 · Model Context Protocol (MCP) tells you how MCP fails. This tells you whether it is failing in your estate - seven phases, run at a terminal, each one ending in a finding you can write down.
Phase 0 - Inventory: find the servers
Most MCP risk in an organization is not a server someone deployed, it is a server someone added to their editor. MCP09 (Shadow MCP Servers) is an inventory problem before it is a security one, and the answer set is small and enumerable: MCP clients keep their server lists in a handful of well-known files.
# macOS / Linux - the common client config locationsls -la ~/.claude.json ~/.claude/settings.json 2>/dev/nullls -la ~/Library/Application\ Support/Claude/claude_desktop_config.json 2>/dev/nullls -la ~/.cursor/mcp.json ./.cursor/mcp.json 2>/dev/nullls -la ~/.vscode/mcp.json ./.vscode/mcp.json 2>/dev/null
# Windows# %APPDATA%\Claude\claude_desktop_config.json# %APPDATA%\Code\User\settings.json
# Sweep a fleet share / home directories for any MCP config, then read what they runfind /Users -maxdepth 4 \( -name 'mcp.json' -o -name 'claude_desktop_config.json' -o -name '.claude.json' \) 2>/dev/null
# What does each config actually launch, and does it carry secrets?jq -r '.mcpServers // .servers | to_entries[] | "\(.key)\t\(.value.command // .value.url) \(.value.args // [] | join(" "))"' \ ~/.cursor/mcp.json 2>/dev/nullgrep -riE 'token|api[_-]?key|secret|password' ~/.cursor/mcp.json ~/.claude.json 2>/dev/nullRecord for every server found: who added it, what it launches, what transport, and whether credentials sit in the config file. Secrets in a client config are MCP01 (token mismanagement) and they are readable by every process running as that user - including any MCP server on the same machine.
Phase 1 - Enumerate: what does the server expose?
# Local stdio server, via the official Inspector CLInpx @modelcontextprotocol/inspector --cli <server-cmd> --method tools/listnpx @modelcontextprotocol/inspector --cli <server-cmd> --method resources/listnpx @modelcontextprotocol/inspector --cli <server-cmd> --method prompts/list
# Remote server, no client involved - this is also your unauthenticated-access testcurl -s -X POST https://<target>/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2025-11-25' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq .If that last command returns a tool list with no credential, stop and write the finding. You have just reproduced the single most common MCP defect, and everything downstream is now reachable by anyone who can route to the host.
Capture the initialize response too - it declares the server’s capabilities, and the reverse-channel ones decide which of the IV.2 · Model Context Protocol (MCP) attacks apply at all:
curl -s -X POST https://<target>/mcp \ -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"2025-11-25", "capabilities":{}, "clientInfo":{"name":"assessment","version":"1.0"}}}' \ | jq '.result | {protocolVersion, capabilities, serverInfo, instructions}'# Flag: sampling (can it drive your model?), sampling.tools (autonomous tool loop),# elicitation (can it prompt your user?), tasks (durable handles), any extensions.# Read `instructions` in full - it lands in the model's context verbatim.Phase 2 - Read what the model reads
This is the highest-value step in the whole assessment and it needs no exploit. The tool description, the inputSchema property descriptions and the server instructions all reach the model before any approval prompt fires - so a server nobody has invoked can still steer the agent.
npx @modelcontextprotocol/inspector --cli <server-cmd> --method tools/list \ | jq -r '.tools[] | "=== \(.name)\n\(.description)\n--- schema descriptions:\n\( [.inputSchema.properties // {} | to_entries[] | " \(.key): \(.value.description // "")"] | join("\n"))"'Review the output for text addressed to the assistant rather than to a developer:
| Grep for | Why it matters |
|---|---|
IMPORTANT, <SYSTEM>, <IMPORTANT> | The classic tool-poisoning wrapper |
do not mention, do not tell the user, without further explanation | Concealment - the tell that separates an attack from bad documentation |
ignore previous, instead of | Instruction override aimed at other tools |
read, ~/.ssh, .env, mcp.json, file paths | Exfiltration staging inside a description |
A parameter with no functional purpose (sidenote, context, debug) | The channel the stolen data leaves through |
Two things reviewers miss: schema property descriptions (most people read only the top-level description) and cross-tool targeting - a poisoned description on server A can impose behavior on a tool belonging to server B, because they share one context.
Tool annotations are not evidence. readOnlyHint, destructiveHint, idempotentHint and openWorldHint are asserted by the server you are assessing. Compare each annotation against what the tool actually does, and treat any mismatch as a finding in its own right - clients use these to decide auto-approval.
Phase 3 - Authorization: the three tests that fail most often
Of 119 OAuth-enabled remote MCP servers measured, every one had at least one authorization flaw. Test in descending order of observed failure rate:
| # | Test | Observed failure rate | What you are checking |
|---|---|---|---|
| 1 | Malicious dynamic client registration | 95.8% | Register a client with an attacker-controlled redirect_uri; does the server bind and honor it? |
| 2 | PKCE downgrade | 68.1% | Drop the code_challenge, or offer plain instead of S256; does the flow still complete? |
| 3 | Consent-page bypass | 60.5% | Replay the authorization request with a different client_id; is consent skipped because a cookie is present? |
# Does the server publish protected-resource metadata, and what does it point at? (RFC 9728)curl -s https://<target>/.well-known/oauth-protected-resource | jq .curl -si https://<target>/mcp -X POST -d '{}' | grep -i 'www-authenticate'
# Audience binding (RFC 8707): does a token minted for a DIFFERENT resource work here?curl -s -X POST https://<target>/mcp \ -H "Authorization: Bearer $TOKEN_FOR_OTHER_AUDIENCE" \ -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq .# A 200 here is token-audience confusion: the server accepted a token not issued to it.
# Scope minimization: does the server publish an omnibus scope catalog?curl -s https://<target>/.well-known/oauth-authorization-server | jq '.scopes_supported'# Wildcards, `all`, `full-access`, or every scope listed up front = over-broad by design.Also test session handling, since the spec is explicit that servers MUST NOT use sessions for authentication: capture an Mcp-Session-Id, then replay it from a different source with no credential. If it works, the session is doing authentication’s job.
Phase 4 - Transport and network
# Origin validation / DNS rebinding - servers MUST validate Origin and MUST 403 an invalid onecurl -si -X POST http://127.0.0.1:<port>/mcp -H 'Origin: https://evil.example' \ -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -1# Expect 403. Then test the gap the spec leaves - omit Origin entirely and see what happens.
# Is a "local" server actually local?ss -ltnp | grep -E ':(3000|8000|8080|11434)' || netstat -ano | findstr LISTENING# A 0.0.0.0 bind on an MCP server is remote access with no auth story.
# Legacy transport - a bare SSE endpoint is a deprecated-transport findingcurl -si https://<target>/sse | head -5
# Estate sweep for exposed MCP and adjacent AI servicesnuclei -tags mcp,exposure -severity medium,high,critical -u https://<target>Phase 5 - Chain it
Individual findings are cheap; the chain is what moves a client. Work the IV.2 · Model Context Protocol (MCP) trust boundaries in order and look for a path from untrusted input to a privileged action:
- Untrusted content in - can attacker-controlled data reach the model through a resource, a tool result, or a fetched page?
- Instruction crossing - does that content get treated as instruction rather than data?
- Privileged tool out - is there a write, send, exec or payment tool in the same context?
- Egress - can the result leave, through a tool argument, an outbound fetch, or a rendered image?
All four present is a reportable exfiltration path, and it is the same lethal-trifecta test as II.2 · Prompt injection & the LLM attack surface - MCP just supplies more of the legs at once. Note that a multi-server host composes these across trust boundaries: server A supplies the injection, server B supplies the capability.
Phase 6 - Pin, then re-check
There is no protocol integrity control for tool descriptions, so a server that passed review can change afterwards. Establish the baseline as part of the assessment:
# Trust-on-first-use pinning (Trail of Bits mcp-context-protector)./mcp-context-protector.sh --command "<server-cmd>"./mcp-context-protector.sh --url https://<target>/mcp# Pinned state: ~/.mcp-context-protector/servers.json - diff it to catch a rug pull
# Or roll your own baseline: hash the exact strings the model consumesnpx @modelcontextprotocol/inspector --cli <server-cmd> --method tools/list \ | jq -S '.tools[] | {name, description, inputSchema}' | sha256sum > mcp-baseline.sha256Scanners help, with one caveat that belongs in the method rather than a footnote:
uvx mcp-scan@latest # scans installed client configsuvx mcp-scan@latest ~/.cursor/mcp.jsonPhase 7 - Report
Map each finding to the taxonomy the client already reports against, so it lands in an existing risk register rather than a new one:
| Finding | Map to |
|---|---|
| No auth on a reachable server | MCP07 insufficient authentication · VIII.1 |
| Instructions in a tool description | MCP03 tool poisoning · AML.T0051 |
| Secrets in a client config | MCP01 token mismanagement |
| Server discovered outside the register | MCP09 shadow MCP servers |
| Injection reaching a privileged tool | MCP06 intent-flow subversion · OWASP LLM01 · ASI01 |
| Token accepted that was not issued to this server | Token passthrough - explicitly forbidden by the spec |
Score with AIVSS so the agentic amplifiers show up rather than being lost in a CVSS base (VIII.4 · ISO/IEC 42001, verification & maturity), and write it twice - technical chain plus the board sentence - per VI.5 · Running the engagement. The evidence-request and findings-table skeletons are in templates.