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.
Written and reviewed by Shreyash Prashu Last reviewed
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. These are 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.
Client registration: CIMD
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. The confused-deputy
attack rides on this mechanism: 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. The authorization server fetches it, sees who the client claims to be,
and applies a trust policy:
- a domain allowlist;
- a reputation check; or
- 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
Issuer validation (RFC 9207). The authorization server returns an iss
parameter with the authorization response. 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
application_type at registration. Clients that still register dynamically
must declare whether they are native or web. This lets an authorization server
treat a localhost redirect URI as intentional rather than as an anomaly.
State handles
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. To prevent misuse:
- 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 real capability is defined
by:
- the description;
- arguments;
- network destinations;
- filesystem roots; and
- side effects.
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. 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. 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 is malicious instruction content in a tool description or result; cross-server shadowing uses one server’s names or content to influence or misroute another server’s capability; a rug pull changes a previously reviewed capability later. Defenses combine origin-aware namespaces, isolated trust contexts, capability snapshots and change review, exact-argument authorization, sandboxing, and runtime monitoring.
In a normal MCP deployment, blindly forwarding the client’s bearer token to a downstream API violates the token’s audience boundary and can create confused-deputy, replay, and audit risks. The server should validate a token intended for itself, authorize the tool call, and obtain a separate, narrowly scoped credential for the downstream resource.
MCP is an open protocol originally introduced by Anthropic that standardizes how an AI host discovers and uses tools, resources, and prompt templates exposed by separate servers. It solves bespoke integration sprawl by giving each server one common interface, while leaving model orchestration, permissions, and user approval to the host.
Autonomous agents are risky because untrusted prompts, retrieved documents, tool outputs, and memories can influence a model that has real authority to read data and take actions. The main risks are prompt injection and hijacking, excessive permissions and confused-deputy actions, data exfiltration, poisoned memory or tools, and runaway cost or destructive loops; defenses must enforce authorization, isolation, approvals, validation, budgets, and auditability outside the model.