Skip to content
MCP Security

MCP Security Risks: 10 Attack Paths and How to Stop Them

A threat model for Model Context Protocol deployments: how each attack works, a published example, and the control that stops it, before or after the tool runs.

Threat model20 min readUpdated Oct 2026

The short answer

MCP connects a model to tools, data and credentials, so its security risks are the ways an attacker can steer what the model reads, what it is allowed to do, and what runs on your machines. Ten attack paths matter most: tool poisoning, indirect prompt injection, rug pulls, confused deputy and token passthrough, over-broad OAuth scopes, session hijacking, SSRF, local stdio servers executing commands, supply-chain compromise of community servers, and data exfiltration through chained tools.

None of them needs a flaw in the protocol itself. Most work because the model treats text from servers and tool results as instructions, or because a tool call runs with whatever access the agent holds; the rest are implementation mistakes in clients, proxies and servers. The controls that hold up sit outside the model: allowlist the servers you run, keep each credential narrow, decide every tool call against policy before it executes, require approval for writes with every argument visible, and scan what comes back.

The MCP specification covers the authorization and transport risks in its Security Best Practices and Authorization pages. The model-facing risks, such as poisoning, injection and rug pulls, come mostly from published research. Both are summarized and linked below.

Scope

This page is the MCP threat model: what can go wrong and which control stops it. For rollout practices see MCP in production; for how the protocol works, the MCP guide; for sandboxing versus a policy layer, MCP firewalls and runtime hardening.

MCP risk table: attack, example, control

One row per attack path. The last column is the point in the MCP lifecycle where the control has to sit; a control placed later cannot catch the attack.

RiskConcrete exampleControl that stops itActs at
Tool poisoningA tool description tells the model to read ~/.ssh/id_rsa and send it in an argument the user never sees.Treat descriptions as untrusted, review them before use, show every argument at approval.Discovery (tools/list)
Indirect prompt injectionA public GitHub issue steers the agent into copying private-repo data into a public pull request.Least privilege, approval for writes, scanning of tool results, session taint.Tool result, paid off on a later call
Rug pullA server changes a tool's description or behavior after you approved it.Pin versions, record tool definitions at approval, re-approve on change.Discovery and updates
Confused deputy and token passthroughA consent cookie lets an attacker's registered client receive a user's authorization code from an MCP proxy.Per-client consent, exact redirect URIs, audience-bound tokens, no forwarding of client tokens.Authorization
Over-broad OAuth scopesAn agent running as Supabase service_role reads a token table the human support role cannot.Minimal scopes with step-up, read-only modes, per-agent tool allowlists.Authorization and call
Session hijackingAn attacker reuses a guessed session ID to inject events into another client's stream or act as that user.Authenticate every request; random IDs bound to the user, never used as credentials.Every request
SSRFA server's 401 response points resource_metadata at the cloud metadata address 169.254.169.254.HTTPS only, block private ranges, check redirects, egress proxy, pin DNS.OAuth discovery
Local stdio servers executing commandsA web page reached the unauthenticated MCP Inspector proxy and launched commands (CVE-2025-49596).Show the exact launch command, sandbox the process, authenticate local listeners, patch.Install and connect
Supply chain of community serverspostmark-mcp shipped 15 normal releases, then version 1.0.16 BCC'd every email to the attacker.Allowlist by exact command or URL, pin versions, one narrow credential per server.Install
Data exfiltration through chained toolsOne tool reads private data, another brings in attacker text, a third sends the data out.Never give one session private data, untrusted content and an outbound channel; approve outbound writes.Across calls

The 10 attack paths in detail

1. Tool poisoning and malicious tool descriptions

When a client connects, it calls tools/list and puts every tool's name, description and input schema into the model's context. A malicious server can hide instructions there. The model reads them before any tool is called, which is why Trail of Bits calls the attack line jumping: an approval prompt at call time comes too late. Because every connected server shares one context, a poisoned description can also change how the agent uses another server's tools (shadowing).

Example: Invariant Labs showed an innocent-looking add tool whose description told the model to read ~/.cursor/mcp.json and ~/.ssh/id_rsa and pass the contents in a sidenote argument; the client's confirmation dialog did not surface it. In the same research, a fake tool's description redirected mail sent through a trusted email tool to the attacker. Trail of Bits documents why invocation-time approval misses it.

Controls:
  • Treat names, descriptions and annotations as untrusted input. The MCP tools spec says clients must treat tool annotations as untrusted unless they come from a trusted server.
  • Read or scan the tool list before a server goes into use, and remove servers whose descriptions carry instructions unrelated to the tool.
  • At approval time, show every argument, not a summary. The spec tells clients to show tool inputs before calling a server for exactly this reason.
  • When you aggregate servers, prefix tool names with a server identifier so a lookalike cannot collide with a trusted tool.

2. Indirect prompt injection through tool results and resources

Tool results and resources carry data an attacker can write: issues, tickets, emails, web pages, documents, database rows. The model cannot reliably separate that data from instructions, so a trusted tool can deliver an attack. Unlike tool poisoning, the server is honest; the content is not.

Example: Invariant Labs filed a malicious issue on a public repository. An agent connected to the official GitHub MCP server read it during a routine task, pulled private-repository data into context and published it in a pull request on the public repo. General Analysis reproduced the pattern with the Supabase MCP server: a support ticket told the agent to read an integration_tokens table and post the contents back into the ticket thread.

Controls:
  • Scope each session to the data the task needs: one repository, one project, read-only where possible.
  • Require approval for writes and outbound actions, with the full arguments on screen.
  • Validate or scan tool results before they reach the model. Treat scanning as a backstop: it catches known phrasing, not intent.
  • Once a session has read injected content, stop trusting it with destructive or outbound tools.

3. Rug pull: the tool changes after approval

Approval usually happens once, when a server is added. MCP lets a server's tool list change over time and announce it with notifications/tools/list_changed, and a package can publish a new version that your launcher installs on the next start. Either way, the definition you approved is not necessarily the one the model reads next week.

Example: Invariant Labs describes a server swapping a tool's description after the client approved it and pairing that with shadowing, so the malicious server need not appear in the interaction log at all. The MCP project's local server security guide notes that a one-time consent dialog and package pinning do not cover this surface.

Controls:
  • Pin server versions. Pinning an npx or uvx package fixes only the top-level package; pinning a container image by digest freezes the whole dependency tree.
  • Record a hash of each tool definition at approval and require re-approval when it changes (trust on first use).
  • Alert on tools/list differences outside a planned upgrade.
  • Prefer clients that re-prompt when a tool definition changes. The protocol does not require it.

4. Confused deputy and token passthrough

An MCP proxy server that fronts a third-party API with one static OAuth client ID acts for many users. If it also lets clients register dynamically and the third-party authorization server remembers consent in a cookie, an attacker can register a client with their own redirect URI, send the user a link, and collect an authorization code without the user consenting again. Token passthrough is the related anti-pattern: a server accepts a token that was not issued to it and forwards it downstream, which breaks audience checks, rate limits and audit attribution, and turns the server into a proxy for anyone holding a stolen token.

Example: The consent-cookie flow is the worked example in the MCP Security Best Practices. The Authorization spec forbids passthrough outright: servers must not accept or transit tokens that were not issued for them.

Controls:
  • MCP proxy servers must collect per-client consent before forwarding to the third-party authorization server, match redirect URIs exactly, and set the OAuth state value only after consent.
  • Servers must validate that every token was issued for them as the audience (RFC 8707) and reject everything else.
  • Clients must send the resource parameter in authorization and token requests so each token is bound to one server.

5. Over-broad OAuth scopes

If a server advertises every scope and the client requests them all up front, every token is a master key. A leaked token, a hijacked session or an injected instruction then reaches everything the scope allows, and the audit trail cannot show which operation the user meant to authorize.

Example: In the Supabase case the agent queried the database as service_role, which bypasses row-level security, so it could read a table the human support role could not.

Controls:
  • Start with a minimal scope set and step up with a WWW-Authenticate scope challenge (403 insufficient_scope) when a privileged operation is first attempted.
  • Avoid wildcard and omnibus scopes, and keep server-side authorization checks: a scope in the token is not the whole decision.
  • Use read-only modes and per-project credentials where a server offers them.
  • Narrow which tools each agent can see and call, whatever the token would allow.

6. Session hijacking

Up to protocol revision 2025-11-25, Streamable HTTP servers could assign each client a session ID. A server that treats that ID as proof of identity lets anyone who obtains or guesses it act as the client. When several servers share an event queue, the attacker can also inject events, including a tools/list_changed notification, that get delivered to the real client. The current revision, 2026-07-28, removes protocol-level sessions; servers that keep state mint handles passed as tool arguments, and the spec calls the same attack state handle hijacking.

Example: From the 2025-11-25 Security Best Practices: the client holds a session on server A, the attacker sends a payload to server B with the same session ID, B queues it, and A delivers it to the client as an asynchronous response.

Controls:
  • Authenticate every request. Never use a session ID or state handle as authentication.
  • Generate IDs and handles with a secure random generator, and expire them.
  • Bind them server-side to the authenticated user, for example a <user_id>:<session_id> key where the user ID comes from the verified token, not the client.

7. Server-side request forgery (SSRF)

During OAuth discovery an MCP client fetches URLs the server supplies: the resource_metadata URL in the WWW-Authenticate header, the authorization server list, and the token and authorization endpoints. A malicious server can point any of them at internal addresses, and the client fetches them from inside your network.

Example: The spec's example returns a 401 whose resource_metadata points at http://169.254.169.254/, the cloud metadata service, so the client fetches instance credentials. DNS rebinding and redirect chains reach the same targets through innocent-looking hostnames.

Controls:
  • Require HTTPS for OAuth URLs outside local development.
  • Block private, loopback, link-local and private IPv6 ranges (RFC 9728 section 7.7) with a maintained library, not a hand-written IP parser.
  • Apply the same checks to every redirect hop, or stop following redirects automatically.
  • Route server-side discovery through an egress proxy such as Smokescreen, and pin DNS results between check and use.

8. Local stdio servers executing commands

A local MCP server is an ordinary process the client launches with your privileges: your environment variables, files, SSH keys and network. The MCP project is explicit that stdio is not a sandbox; the client and a stdio server share one trust domain. The attacks are a malicious launch command in a client configuration, a malicious payload inside the server, and local listeners that other processes or a web page can reach.

Example: CVE-2025-49596: MCP Inspector versions before 0.14.1 ran an unauthenticated local proxy that could launch commands over stdio, and a malicious web page could reach it through 0.0.0.0 or DNS rebinding. CVE-2025-6514: in mcp-remote 0.0.5 to 0.1.15, a malicious server's crafted authorization_endpoint led to command execution on the client machine.

Controls:
  • Before a one-click install, the client must show the exact launch command without truncation and get explicit approval.
  • Run third-party servers in a container, VM or OS sandbox with only the directory they need, and no network unless they need it.
  • Binding to 127.0.0.1 is not authentication: local HTTP listeners need a token and origin checks.
  • Never open server-supplied URLs through a shell, and patch MCP bridges and developer tools promptly.

9. Supply chain of community servers

Most MCP servers are community packages installed from npm, PyPI, GitHub or a container registry, often by name and often unpinned. Typosquats, lookalike repositories, account takeovers and compromised dependencies all deliver code that runs with the credentials you hand the server. A registry listing is not a security review.

Example: A package named postmark-mcp imitated Postmark's MCP server. After 15 versions that behaved normally, version 1.0.16 added one line that BCC'd every email sent through it to an attacker-controlled address (found by Koi Security and reported by CSO Online in September 2025). The OWASP MCP Top 10 lists supply chain attacks and shadow MCP servers as separate entries.

Controls:
  • Keep an inventory of every server across desktop clients, IDEs and CI, and remove the ones nobody uses.
  • Allowlist servers by exact launch command or URL. In Claude Code, serverCommand and serverUrl entries enforce; a serverName entry is only a label a user can reuse.
  • Pin versions, prefer publishers you can identify, and verify signatures or SBOMs where they exist.
  • Give each server its own least-privilege credential and watch where it sends traffic.

10. Data exfiltration through chained tools

Each tool can be harmless alone; the chain is the attack. One tool exposes private data, another brings in attacker-written text, a third can send data out: an HTTP request, an email, a pull request, even an image URL the client renders. Simon Willison calls the combination the lethal trifecta: private data, untrusted content and external communication in the same agent.

Example: The GitHub MCP case is the lethal trifecta in one server: the malicious issue is the untrusted content, the private repository is the data, and the public pull request is the outbound channel.

Controls:
  • Do not give one session all three. Removing any one leg breaks the attack.
  • Require approval for outbound writes and sends, with the full arguments shown.
  • Restrict where tools can send data: egress allowlists, fixed recipients, one repository per session.
  • After a session reads untrusted content, deny its outbound and destructive tools.

Before or after the call: where controls act

A control helps only where the attack happens. Tool poisoning lands at discovery, before any call. Injection arrives in a result and pays off on a later call. Exfiltration needs a chain of calls. Sort your controls by stage:

StageWhat goes wrongControls
Install and connectMalicious package or launch command, unsafe endpoint, unauthenticated local listenerAllowlist by command or URL, version pinning, endpoint validation, sandboxing
Discovery (tools/list)Poisoned descriptions, shadowing, rug pullsDescription review, definition hashing with re-approval, server-prefixed tool names
AuthorizationConfused deputy, token passthrough, broad scopes, SSRF during discoveryPer-client consent, audience-bound tokens, minimal scopes, URL validation
Call (tools/call): pre-executionHijacked intent, dangerous arguments, writes nobody approvedPolicy decision on server, tool and arguments: allow, deny or hold for approval
Result: post-executionInjected instructions, secrets or PII in outputResult scanning, redaction or quarantine, session taint

Pre-execution policy

The only point where a side effect can still be prevented. It sees the server, the tool and the arguments, and returns allow, deny or require approval before anything runs. It cannot see what the model was told or what the server does internally.

Post-execution scanning

Runs on what comes back. It can redact a secret before the model sees it, quarantine a result, or mark the session as tainted so the next risky call is refused. It cannot recall an email that was already sent or undo a merge.

Where Cordum MCP governance fits

Cordum is the action firewall for AI-agent actions. For MCP that means a policy decision before the tool runs, plus scanning of what comes back. It covers three surfaces:

  • Cordum Edge for Claude Code. Claude Code fires its PreToolUse hook for MCP tools too, named mcp__<server>__<tool>, and Edge registers its hook for every tool, so Edge policy can deny or hold a specific MCP tool before Claude Code runs it. For a fleet, Edge's managed-settings bundle includes a managed-mcp.json that points Claude Code only at the Cordum MCP gateway; deployed by MDM, it puts Claude Code in exclusive-control mode, so users cannot add other servers. The local cordumctl edge claude wrapper is a developer path, not an enforcement boundary. See Edge for Claude Code.
  • MCP Governance at the Cordum gateway. The gateway MCP endpoint (/mcp/message, /mcp/sse) authenticates every request, checks the tenant and filters tools per agent. Built-in mutating tools wait for human approval by default. With mcp.policy_gate_enabled=true, each tools/call also passes the action-gate pipeline (tenant, file, URL, MCP, mutation and provenance gates) and the session-taint check before the handler runs. Approval holds are bound to the tenant, tool, canonical arguments and active policy snapshot, are consumed once, and cannot be approved by the agent that requested them.
  • Safety Kernel and output safety. Jobs that carry mcp.server, mcp.tool, mcp.resource or mcp.action labels (the MCP bridge pack sets mcp.server, mcp.tool and mcp.action on every call) are checked against tenant-level and rule-level MCP allow and deny lists before dispatch. When output policy is enabled, their results go through output safety, which can release, redact or quarantine them using the secret_leak, pii and injection scanners.

Gated calls emit mcp.tool.pre, mcp.tool.post or mcp.tool.failed events with redacted arguments, and MCP approvals and scope denials are written to the tenant's hash-chained audit log. Here is how that maps to the ten risks:

RiskWhat Cordum doesWhen it actsLimit
Tool poisoningDoes not read or rewrite tool descriptions. Decides the call they produce: the MCP gate denies servers and tools outside the calling agent's allowlists, and approval records show the full canonical arguments, so a smuggled parameter is in front of the approver.Pre-executionNo description scanning.
Indirect prompt injectionWith the policy gate on, tool results are scanned with the Safety Kernel prompt-injection scanner. A finding taints the session, and later destructive calls in that session are denied.Result scan, enforced before the next callPattern-based. By default, destructive means a tool name matching *delete*, *remove* or *archive*, or a GraphQL delete, remove or archive mutation in the arguments; both lists are configurable.
Rug pullEach call is decided on server, tool and arguments when it is made. Approvals are single-use and bound to the exact arguments and the active policy snapshot, so trust does not carry forward.Pre-executionNo tool-definition hashing or re-approval on change.
Confused deputy and token passthroughTenant and principal come from the caller's Cordum credentials, not the request body. A registered upstream is called with the gateway's own secret:// credential, so client tokens are never forwarded.Every requestThe gateway becomes the deputy; per-agent scope and approvals are what bound it.
Over-broad OAuth scopesPer-agent scope: the built-in tool registry is filtered at tools/list and tools/call by tool globs, risk tier and data classifications, where an empty list means no tools and runtime overrides can only tighten. The policy gate also checks server, resource and entitlement allowlists on each call.Discovery and callDoes not narrow the upstream token itself.
Session hijackingEvery /mcp/message request is authenticated, and identity and tenant come from the request's own credentials, never from the session ID, so a reused ID grants nothing. Approval holds are keyed to the calling agent, so one agent cannot consume another's approval.Every requestSession IDs are random and only route SSE events; treat them as secrets. Covers Cordum's endpoint, not your other MCP servers.
SSRFThe upstream registry rejects cloud-metadata hostnames and endpoints that resolve to loopback, private, link-local or multicast addresses, re-resolves them before use and dials the pinned IPs. Upstream auth uses a stored credential, not server-supplied OAuth discovery URLs.Registration and connectCovers Cordum's own upstream calls, not other MCP clients.
Local stdio serversEdge's managed-settings bundle includes a managed-mcp.json that limits Claude Code to the Cordum MCP gateway. Registry stdio entries must be a shell-free argv; shell metacharacters are rejected.Install and connectFleet enforcement needs MDM-deployed managed settings. Cordum does not sandbox processes.
Supply chainA tenant-scoped registry of approved upstreams with secret:// credential references. Enterprise-strict mode accepts only allowlisted names, and every update keeps a backup of the previous record.RegistrationCannot see what a server does inside its own process.
Chained exfiltrationApproval holds that show the full arguments before a write runs, Edge rules on Claude Code MCP tool names, session taint, and output safety on job results (REDACT or QUARANTINE).Pre- and post-executionPost-execution scanning cannot undo a send.

Two policy shapes from the Safety Kernel schema: a tenant-level MCP allowlist for MCP calls that run as jobs, and a rule that holds one GitHub MCP tool in Claude Code sessions governed by Edge.

policy fragment
YAML
version: v1

tenants:
  default:
    mcp:
      # Checked for jobs that carry mcp.server / mcp.tool / mcp.action labels.
      # Deny wins; a non-empty allow list blocks everything not on it.
      allow_servers: ["github", "jira"]
      deny_servers: ["internal-admin"]
      deny_tools: ["delete_issue"]
      deny_actions: ["delete"]

rules:
  # Claude Code under Cordum Edge: MCP tools arrive as hook events on
  # job.edge.action, labeled hook.tool_name=mcp__<server>__<tool>.
  - id: claude-code.hold-github-mcp-merges
    match:
      topics: ["job.edge.action"]
      labels:
        "hook.tool_name": "mcp__github__merge_pull_request"
    decision: require_approval
    reason: "Merging through the GitHub MCP server needs a human approver."

Both are fragments to merge into an existing policy bundle: an action no rule matches falls to default_decision, which is deny unless you set it. Before you rely on either, test the allow, deny and approval paths, including an approved retry with identical arguments.

What Cordum does not do

  • It does not pin, hash or diff third-party tool descriptions. Poisoning and rug pulls are bounded by call-time policy and approvals, not detected.
  • It cannot see inside a server process. A server that copies data on its own, like postmark-mcp's BCC, is stopped only by not running it.
  • Fronting a third-party remote server through the gateway is opt-in by registration and narrower than the built-in server. Treat arbitrary external MCP mediation as preview scope and test it before you rely on it.
  • Its scanners are pattern-based. They catch known phrasing and secret formats, not every injection.
  • The policy gate is off until you set mcp.policy_gate_enabled=true, so existing deployments keep their behavior until you opt in.

MCP hardening checklist

  1. Inventory every MCP server across desktop clients, IDEs and CI. Remove what nobody uses.
  2. Allowlist servers by exact launch command or URL, and deny everything else.
  3. Pin versions. Diff tools/list on every upgrade and re-approve changed definitions.
  4. Give each server its own narrow credential. Use audience-bound OAuth tokens and never forward a client token.
  5. Run local servers sandboxed, with one directory and no network unless they need it. Authenticate anything that listens on a port.
  6. Decide every tool call before it runs: allow, deny or hold for approval, on server, tool and arguments.
  7. Require approval for writes and outbound sends, with the full arguments on screen.
  8. Never give one session private data, untrusted content and an outbound channel at once. When injected content shows up, cut off destructive and outbound tools.
  9. Scan results before they reach the model and redact secrets. Treat scanning as a backstop.
  10. Log every call with redacted arguments and the decision that allowed it.

Frequently Asked Questions

What are the main security risks of MCP?
The ten that matter most are tool poisoning, indirect prompt injection through tool results, rug pulls, confused deputy and token passthrough, over-broad OAuth scopes, session hijacking, SSRF during OAuth discovery, local stdio servers running commands, supply-chain compromise of community servers, and data exfiltration through chained tools. Most of them steer what the model reads or abuse access the agent already has; none needs a bug in the protocol.
What is MCP tool poisoning?
Tool poisoning hides instructions in a tool's name, description or schema. The client loads those definitions into the model's context when it lists tools, so the model can act on them before the tool is ever called and before any call-time approval prompt appears. Treat tool definitions as untrusted, review them before use, and show every argument when a call needs approval.
How is tool poisoning different from prompt injection?
In tool poisoning the server is the attacker and the payload sits in the tool definition. In indirect prompt injection the server can be honest: the payload sits in data a tool returns, such as an issue, a ticket or a web page. Poisoning is stopped at install and discovery time; injection has to be handled on results and on the calls that follow.
What is an MCP rug pull?
A rug pull is a server or package that changes after you approved it: a tool description or behavior is swapped, or a new release adds malicious code. A one-time approval does not cover it. Pin versions, record tool definitions at approval, and require re-approval when they change.
Does OAuth make an MCP server secure?
OAuth answers who the client is and which server a token is for. The MCP authorization spec requires audience-bound tokens and forbids token passthrough, but it does not stop a properly authorized agent from being steered into a harmful call, and stdio servers do not use it at all; they take credentials from the environment. You still need least-privilege scopes, per-call policy and approvals.
Are local stdio MCP servers safer than remote ones?
Stdio limits who can talk to the server, but the server runs as you, with your files, environment variables and network. The MCP project's local server security guide treats a third-party local server as untrusted code: pin it, sandbox it and give it its own narrow credentials. A remote server avoids running third-party code on your machine but brings the OAuth risks instead.
How do I prevent data exfiltration through MCP tools?
Break the lethal trifecta: do not let one session combine private data, untrusted content and a way to send data out. Then require approval for outbound writes with the full arguments shown, restrict destinations, and stop trusting a session with destructive or outbound tools once it has read injected content.
Where does Cordum fit in MCP security?
Cordum puts a policy decision in front of MCP tool calls: Cordum Edge for Claude Code tool calls, including MCP tools, and the Cordum gateway for MCP clients that connect to it, with per-agent tool scoping, approval holds bound to the exact arguments, and session taint after injected results. MCP calls that run as Cordum jobs also get Safety Kernel MCP allow and deny lists before dispatch and output scanning after. Cordum does not replace server-side permissions, sandboxing or tool-definition pinning.

Related MCP guides

Put a policy decision in front of MCP tool calls

Cordum MCP Governance scopes each agent's tools, holds risky calls for approval with the exact arguments, and records redacted evidence before the tool runs.

Sources