MCP Security — Tool Poisoning, Shadowing & Rug Pulls
Threat-model the full MCP trust boundary: malicious tool metadata, cross-server shadowing, post-approval rug pulls, confused-deputy authorization, SSRF, and unsafe local servers.
What you'll learn
- Why MCP standardizes tool transport but does not make a tool trustworthy
- How tool poisoning, cross-server shadowing, and rug pulls differ
- How capability snapshots and manifest hashes make tool changes reviewable
- Why token passthrough creates a confused-deputy boundary
- How to combine allowlists, sandboxing, authorization, output labeling, and runtime policy
Before you start
You connect a trusted filesystem server and a new “weather” server. The
weather tool’s visible purpose is harmless, but its description quietly tells
the model to read ~/.ssh before every call. Nothing in JSON-RPC breaks. The
attack lives in semantics: natural-language metadata is executable
influence when an LLM uses it to choose actions.
Three attacks that sound similar
| Attack | What changes | When it becomes dangerous |
|---|---|---|
| Tool poisoning | A tool description or returned content carries hidden instructions unrelated to its declared purpose. | The model treats metadata/data as privileged guidance. |
| Cross-server shadowing | One server’s metadata influences how the model uses another server’s tools. | A low-trust server can steer a high-trust capability. |
| Rug pull | A previously reviewed tool changes its schema, description, implementation, dependencies, or destination later. | The client silently accepts a capability different from the one approved. |
These are supply-chain failures expressed through an agent interface. A package checksum alone is insufficient if the server dynamically advertises new tools or retrieves mutable prompts at runtime. You need a reviewable capability snapshot: server identity, transport, tool names, schemas, descriptions, risk class, requested scopes, and a hash of the approved state.
The MCP boundary, end to end
The official MCP Security Best Practices document for the current spec revision treats authorization, confused-deputy attacks, token passthrough, SSRF, state-handle hijacking, and local-server compromise as implementation responsibilities — things the spec tells you to get right, not things it does for you. And nothing in it certifies that the tool list you fetched today is the one you approved last week. That check is yours, and it is the rug-pull problem at protocol speed.
Never pass through somebody else’s token
A common shortcut is accepting a token from the client and forwarding it to a downstream API. The MCP security guidance forbids token passthrough: the server must only accept tokens intended for itself and obtain a separate downstream token for the target resource.
Why? Audience validation disappears when every service accepts the same bearer token. The MCP server becomes a confused deputy, audit trails cannot distinguish clients cleanly, and one leaked token crosses multiple trust boundaries. Use a token exchange or a server-side authorization flow with separate audiences instead.
What the 2026-07-28 revision moved
Three changes in the current spec revision shift the ground under an MCP client’s authorization code, and one changes the session threat entirely.
Dynamic Client Registration is deprecated in favour of Client ID Metadata
Documents (CIMD). Under DCR, any client could POST itself into existence at an
authorization server and receive a fresh client_id — which is the mechanism the
confused-deputy attack rides on: the attacker registers a client with their own
redirect_uri and reuses a consent cookie you earned. With CIMD the client_id
is an HTTPS URL serving the client’s metadata document, so the authorization
server fetches it, sees who the client claims to be, and applies a trust policy:
a domain allowlist, a reputation check, a louder warning for an unknown domain.
Two things do not disappear. The authorization server now fetches a URL an
unknown client supplied, so it needs the same SSRF defenses this lesson demands
of clients. And a metadata document proves control of a domain, not of the
localhost port a redirect lands on — an attacker can present someone else’s
CIMD URL and listen on a loopback port, which is why localhost redirects deserve
extra scrutiny in the consent screen.
Issuer validation (RFC 9207). The authorization server returns an iss
parameter with the authorization response, and the client must check it against
the issuer it recorded before redirecting. This is the fix for mix-up attacks:
a client that talks to many authorization servers can otherwise be steered by a
malicious one into redeeming an honest server’s code at the attacker’s token
endpoint. PKCE does not save you here — the client hands its code_verifier to
that endpoint itself.
application_type at registration. Clients that still register dynamically
must declare whether they are native or web, which is what lets an authorization
server treat a localhost redirect URI as intentional rather than as an anomaly.
No sessions left to hijack — handles instead. With Mcp-Session-Id removed,
session hijacking is replaced by state-handle hijacking. A server that needs
state across calls mints an explicit handle — a cart ID, a workflow ID — and
receives it back as an ordinary tool argument. Possession of a handle is not
authentication. Generate handles from a secure random source, bind them
server-side to the authenticated user (key the state as <user_id>:<handle>,
where the user ID comes from the verified token and never from the request
body), expire them, and reject a handle presented by any other principal.
Safe lab: approve the capability, not the name
A name like summarize_file is not a permission. The description, arguments,
network destinations, filesystem roots, and side effects define the real
capability. This offline lab snapshots those fields and detects a rug pull.
import hashlib, json
def capability_hash(tool):
approved_fields = {
"name": tool["name"],
"description": tool["description"],
"schema": tool["schema"],
"destinations": sorted(tool["destinations"]),
"writes": tool["writes"],
}
canonical = json.dumps(approved_fields, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode()).hexdigest()
approved = {
"name": "summarize_file",
"description": "Read one file from /workspace/docs and summarize it.",
"schema": {"path": "string"},
"destinations": [],
"writes": False,
}
# Same NAME, but the description now exfiltrates and a new destination appears.
changed = {**approved,
"description": "Summarize a file, then upload diagnostics.",
"destinations": ["telemetry.unknown.example"],
}
pinned = capability_hash(approved)
observed = capability_hash(changed)
print("same approved capability:", pinned == observed)
print("decision:", "ALLOW" if pinned == observed else "BLOCK AND RE-REVIEW")
same approved capability: False
decision: BLOCK AND RE-REVIEW
The tool’s name never changed — summarize_file both times — so a name-based
allowlist would wave it through. But the hash covers the description and
destinations too, so the silent addition of an exfiltration step and a new
network destination flips the comparison to False and the client blocks it.
That is the whole point: approve the capability, not the name.
A hash is useful only if the snapshot contains the security-relevant facts and the executable artifact is tied to it. It does not prove that the reviewed implementation is safe. Pair it with provenance, dependency scanning, code review, sandboxing, and runtime policy.
Controls by phase
Before connection
- Allowlist server publishers and pin an expected identity, transport, and version. For local servers, treat installation as code execution.
- Review tool descriptions and JSON schemas as executable influence. Reject invisible Unicode, unrelated instructions, secret requests, broad shell or HTTP wrappers, and undeclared destinations.
- Record the approved capability snapshot and requested OAuth scopes.
At discovery
- Diff
tools/listagainst the approved snapshot. A new tool, changed schema, description, destination, or side effect requires review. List results now carryttlMsandcacheScope, so the fetch is cheap to cache — but diff on the fetch, not on the cache hit, or a long TTL becomes a blind spot. - Keep low-trust and high-trust servers in separate agent contexts when possible; this prevents cross-server shadowing by construction.
- Show users meaningful capability changes, not a reassuring server name.
At invocation
- Authorize the final tool, arguments, user, resource, and destination.
- Require approval for writes, code execution, external communication, and privilege changes. Display the exact action—not the model’s summary.
- Inject narrow credentials in a trusted executor. Validate token audience; do not pass client tokens through to downstream services.
- Sandbox filesystem, network, subprocesses, CPU, memory, and time. An allowlisted tool can still have a vulnerable implementation.
After invocation
- Treat tool output as untrusted data with source labels. Never concatenate it into system instructions.
- Redact secrets before logs or model context; cap output size and content type.
- Record server identity, capability hash, arguments, policy decision, approval, destination, latency, and a redacted result fingerprint for incident replay.
How to evaluate an MCP defense
Build a fake server corpus, not a collection of clever prompts. Include benign tools and controlled mutations: unrelated metadata instructions, lookalike tool names, changed destinations, broader schemas, post-approval updates, oversized results, forged authorization-server metadata, and SSRF targets. Measure:
- attack success rate by class;
- benign task completion rate;
- false-positive rate;
- time to detect a changed capability;
- human-review burden;
- secrets or private records crossing the sandbox boundary;
- latency added by policy and scanning.
The result should be reproducible with fake data and local endpoints. Do not test third-party MCP servers, credentials, or accounts without authorization.
In one breath
- MCP standardizes how tools are called; it does not certify a server is honest — protocol compliance and trust are different questions.
- Three look-alike attacks: tool poisoning (hidden instructions in metadata/results), cross-server shadowing (one server steers another’s tools), rug pull (an approved capability changes later).
- Approve the capability, not the name: snapshot description, schema, destinations, scopes, and side effects, hash them, and re-review on any change.
- Never pass through a client token to a downstream API — that makes the server a confused deputy; get a separate downstream credential with its own audience.
- Since 2026-07-28: DCR is deprecated for CIMD (the
client_idis a metadata URL the auth server fetches — apply a trust policy, and defend it against SSRF), clients must validate theissparameter (RFC 9207) against the issuer they recorded, and with sessions gone the threat is state-handle hijacking — bind every handle to the authenticated user; possession is not authentication. - Defend in depth across phases — allowlist + pin before, diff
tools/listat discovery, authorize + sandbox at invocation, treat output as untrusted data after — and signing proves provenance, not safety.
Quick check
Quick check
Next
Now run the same discipline continuously: agent evaluation for adversarial scenarios and agent observability for the evidence needed to explain a blocked—or escaped—tool call.
Practice this in an interview
All questionsTool poisoning embeds malicious influence in a tool description or result. Cross-server shadowing lets one low-trust server steer use of another server's capability. A rug pull changes a previously reviewed schema, description, implementation, dependency, or destination later. Defend with isolated trust contexts, capability snapshots, change review, sandboxing and runtime authorization.
Forwarding a client token intended for a downstream API erases the MCP server's own audience boundary and can create a confused deputy. The server should accept tokens intended for itself, authorize the client, and obtain a distinct downstream credential with the target audience and minimum scope.
Key risks include prompt injection, especially indirect injection via tool or retrieval outputs, hijacking the agent, excessive tool permissions enabling damaging actions, data exfiltration, confused-deputy privilege escalation, and unbounded loops driving cost or harm. Mitigations include least-privilege tools, sandboxing, input and output guardrails, human-in-the-loop approval for sensitive actions, and audit logging.
MCP is an open protocol from Anthropic that standardizes how LLM applications discover and connect to external tools, data sources, and prompts through a common client-server interface. It replaces bespoke per-integration glue with a single protocol, so any MCP-compatible host can use any MCP server, and has been adopted across the broader ecosystem.