- The current MCP revision, 2026-07-28, drops protocol-level sessions. Every request declares its own protocol version.
- Authorization is optional in the spec, but HTTP servers should implement it. When they do, they must validate that each token was issued for them, and they must never pass client tokens through to other services.
- The real incidents so far are ordinary security bugs in an AI setting: command injection in a client proxy (CVE-2025-6514), an unauthenticated local developer tool (CVE-2025-49596), and instructions hidden in tool descriptions.
- Test an MCP server twice: once as an API, for authorization, input handling, and SSRF, and once as a prompt-injection surface, for tool descriptions, tool output, and changes after approval.
What is MCP, and why does it need its own security review?
The Model Context Protocol (MCP) is an open protocol that lets an AI application discover and call tools, read resources, and use prompts exposed by an MCP server. One server might wrap a ticketing system, a database, a code repository, or a cloud account. Whatever the server can do, an agent can now do too, often on instructions that came from a document, web page, or other tool output the user never read.
An MCP server is therefore both an API and a capability you give a language model. Classic API flaws such as broken authorization and injection still apply. On top of those, the model reads everything the server returns and may act on it.
What changed in the MCP specification in 2026?
MCP version identifiers are dates, and each marks the last backwards-incompatible change. The current revision is 2026-07-28. The security-relevant changes:
- Stateless requests. The initialize handshake used up to 2025-11-25 is gone. Each request declares its protocol version in
_meta, and on Streamable HTTP also in theMCP-Protocol-Versionheader. A mandatoryserver/discovercall returns the server's versions, capabilities, and identity. - Authorization is optional, but defined. HTTP transports should conform to the MCP authorization specification, which builds on the OAuth 2.1 draft. Servers using stdio should not use it; they take credentials from the environment instead.
- Discovery through Protected Resource Metadata. MCP servers must publish RFC 9728 Protected Resource Metadata, and clients must use it to find the authorization server.
- Audience-bound tokens. Clients must send the RFC 8707
resourceparameter (the server's canonical URI) in both the authorization request and the token request. Servers must verify that a token's audience is themselves and must not accept or forward any other tokens. - Client registration. Client ID Metadata Documents are the recommended way to identify clients. Dynamic Client Registration (RFC 7591) is still permitted but deprecated. PKCE is required, and clients must validate the RFC 9207
issparameter to stop mix-up attacks.
Which attacks does the MCP specification warn about?
The official security best practices page lists the attack classes implementers must design against. The ones we test most often:
| Attack class | What goes wrong |
|---|---|
| Confused deputy | A proxy server uses one static client ID with a third-party authorization server, so consent granted for one client is silently reused for another. The spec requires per-client consent. |
| Token passthrough | The server accepts a token that was not issued for it and forwards it downstream. This is explicitly forbidden. It bypasses audience checks, rate limits, and audit trails. |
| SSRF during discovery | Metadata URLs supplied by a malicious server point at internal addresses such as 169.254.169.254, often reached through DNS rebinding or redirect chains. |
| Authorization URL abuse | A client opens an authorization URL supplied by the server. javascript: URLs lead to XSS, and opening URLs through a shell leads to code execution. Only http and https should be allowed. |
| Local server compromise | One-click installation runs an arbitrary startup command. The spec requires showing the full command and getting consent before running it. |
| State handle hijacking | The 2026-07-28 revision replaces the older session-hijacking guidance with guidance on state handles. The principle is unchanged: an identifier that ties requests together must never be enough, on its own, to act as a user. |
What has already gone wrong in real MCP deployments?
Tool poisoning, rug pulls, and cross-server shadowing
In April 2025, Invariant Labs showed that a tool's description can carry hidden instructions. The model reads them; the user usually does not. The proof of concept got an AI coding assistant to read SSH keys and MCP configuration and leak them through tool parameters. The same research described rug pulls, where a description changes after the user approves the tool, and shadowing, where one server's description changes how the agent uses another server's tools. OWASP now tracks this as MCP03:2025 Tool Poisoning in its MCP Top 10.
CVE-2025-6514: command injection in mcp-remote
mcp-remote is a widely used proxy that lets stdio-only clients talk to remote MCP servers. Versions 0.0.5 up to, but not including, 0.1.16 passed an authorization_endpoint from the remote server into an OS command. A malicious server could run commands on the user's machine just because the user connected to it. It is rated CVSS 9.6 (CWE-78) and fixed in 0.1.16.
CVE-2025-49596: unauthenticated MCP Inspector proxy
MCP Inspector, a developer tool, ran a local proxy with no authentication. A malicious website could reach it from the browser and start stdio servers, leading to remote code execution on the developer's machine. It was fixed in version 0.14.1. The lesson reaches beyond MCP: a server bound to localhost is still reachable by your browser.
How do we test an MCP server?
We start with discovery and authorization, then test each tool as an API endpoint, then test what the model sees.
1. Discovery and the 401 response
An unauthenticated request should be rejected and should point the client to the Protected Resource Metadata:
curl -si https://mcp.example.com/mcp -X POST \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
curl -s https://mcp.example.com/.well-known/oauth-protected-resource
{"resource":"https://mcp.example.com/mcp","authorization_servers":["https://auth.example.com"]}
We check that the resource value is the server's canonical URI, that the authorization server list contains only systems you control, and that no tool, resource, or server/discover detail leaks before authentication beyond what you intend to be public.
2. Token audience and passthrough
We obtain a valid token for a different resource on the same authorization server and replay it against the MCP server. It must be rejected. We also look for code that forwards the incoming bearer token to downstream APIs. The server should hold its own credentials for those APIs.
// Server-side check we expect to find (pseudocode)
const claims = verifyJwt(token, jwks); // signature, exp, nbf
if (!claims.aud?.includes(CANONICAL_MCP_URI)) {
return unauthorized('token not issued for this server');
}
// Never: fetch(downstream, { headers: { Authorization: `Bearer ${token}` } })
3. Per-tool authorization
Each tool is an endpoint. We call tools with a low-privilege user's token and change IDs, tenants, file paths, and repository names in the arguments. The server must enforce what this user may do. What the model decided to request is irrelevant. Scopes should match the tools: a read-only integration should not hold write scopes.
4. Input handling inside tools
Tools that touch a shell, the filesystem, SQL, or URLs get the same injection testing as any backend: command injection, path traversal, SQL injection, and SSRF. Arguments arrive from a model that may be following attacker instructions, so treat them as untrusted input.
5. What the model reads
We seed tool descriptions and tool output with instructions, for example a ticket body that tells the agent to call a different tool, and observe whether the agent follows them. We also change a tool description after approval to check whether the host notices. Good hosts pin approved descriptions and ask the user again when one changes.
6. Client and host checks
- Authorization URLs from a server are only opened when they are
httporhttps, and never through a shell. - Local server installation shows the exact command and arguments before running.
- Metadata fetches cannot reach link-local, loopback, or private addresses, including after redirects.
- Local developer tools require authentication even on
localhost.
What should teams shipping MCP servers do first?
- Implement the authorization spec for any HTTP server, and validate token audience on every request.
- Hold separate downstream credentials. Never forward the client's token.
- Enforce authorization inside every tool, per user and per tenant.
- Keep tool descriptions short, static, and reviewed like code. Version them, and treat any change as a security-relevant release.
- Apply SSRF protections to every URL the server or client fetches, including OAuth metadata.
- Log the actor, the tool, the arguments, and the result, so an agent's actions can be reconstructed afterwards.
How we test this: MCP servers and agent tool integrations are part of our AI and LLM security testing and API security testing. We test authorization, injection, and prompt-injection paths together, because that is how attackers chain them.
