MCP Security: OAuth, Tool Permissions, and Prompt Injection
What is MCP security?
MCP security is the set of authorization, isolation, validation, approval, and audit controls that limit what Model Context Protocol clients and servers can read or change. It covers both remote OAuth-protected servers and local processes running with a user's privileges.
TL;DR
- -MCP security is capability security: the real question is what a compromised model, client, server, or tool result can cause
- -For remote HTTP servers, validate token issuer, audience, expiry, and scopes on every request; never pass the client's MCP token to an upstream API
- -Authorization and tool approval are separate controls: scopes decide what a caller may do, while approval decides whether this invocation should happen now
- -Treat tool descriptions, arguments, and results as untrusted; enforce policy in deterministic code rather than asking the model to police itself
- -Run local servers with the smallest filesystem, network, and secret access possible, and keep audit events useful without storing raw credentials or unnecessary PII
An MCP server turns model output into capability. A calendar search is modest. Sending mail, running SQL, changing cloud infrastructure, or reading a home directory is not. The security question is therefore not “is this prompt safe?” It is:
If one component is fooled or compromised, what can it read, change, or send?
That framing catches failures OAuth alone cannot. A valid user can still be tricked into approving the wrong action. A trusted server can return poisoned content. A local server can be legitimate and still have far too much access.
This guide is the security layer for the broader MCP server architecture and production server guide. It follows the current MCP specification dated July 28, 2026; version-sensitive links below point to that revision rather than to an undated summary.
Draw the trust boundaries first
A typical remote path has four principals:
user → MCP host/client → MCP server → upstream API or database
↘ model context and tool results
They do not share one identity or one trust level.
- The host decides what context and tools the model can see.
- The client speaks MCP and carries authorization for the current caller.
- The server validates that caller and enforces tool policy.
- The upstream system has its own identity and permission model.
- The model proposes actions; it is not an authorization engine.
Write down which boundary checks identity, tenant, object ownership, scope, and user approval. If a box says “the prompt handles it,” the design has no hard control there.
Threat model: the failures worth designing for
| Failure | What it can become | Primary control |
|---|---|---|
| Malicious tool description | The model selects a tool or leaks context it should not | Trust registry, review, tool allowlist, isolated contexts |
| Injection in a document or tool result | A read operation steers a later write operation | Treat results as data, separate read and act stages, approve sensitive calls |
| Broad scopes or wildcard tools | One token unlocks unrelated systems | Per-capability scopes and step-up authorization |
| Token passthrough | A token is replayed at the wrong service | Audience binding and separate upstream credentials |
| Unchecked tool arguments | SQL, command, path, URL, or tenant injection | Schema plus semantic validation and server-side policy |
| Guessable workflow handle | Cross-user state access | Opaque expiring handles bound to the authenticated principal |
| OAuth metadata SSRF | Access to internal services or cloud metadata | HTTPS, destination validation, redirect checks, egress policy |
| Over-privileged local server | Source, SSH keys, or credentials become readable | Sandboxing and explicit filesystem/network grants |
| Verbose telemetry | Credentials or customer data persist in logs | Field allowlist, redaction, access control, retention |
This list is intentionally operational. “The model may hallucinate” is not a useful threat until it is connected to a capability and an impact.
Remote authorization: implement the resource-server boundary
For HTTP transports, MCP defines an OAuth-based flow. The current authorization specification requires Protected Resource Metadata for authorization-server discovery and Resource Indicators so the requested token is bound to the intended MCP server.
At the server, every protected request needs deterministic checks:
signature → issuer → audience/resource → expiry → scopes → tenant/object policy
The first five validate the credential. The last one validates the requested action.
A token with tickets:write does not prove that the caller may update every ticket in
every tenant.
Keep scopes close to capabilities
Avoid a single mcp:* or admin scope. Prefer names that match stable permissions:
issues:read
issues:comment
issues:close
deployments:read
deployments:promote
Start with discovery and read access. Request a stronger scope when the user first
attempts a privileged operation. The current spec supports this through a 403 plus an
insufficient_scope challenge. Its
scope-minimization guidance
explains why asking for everything up front increases blast radius and makes consent
less meaningful.
Never pass the MCP token downstream
Token passthrough is not a shortcut. It erases the boundary between the MCP server and the API behind it. The MCP authorization spec requires the server to accept only tokens intended for itself and forbids transmitting other tokens onward.
If the server calls GitHub, Google, or an internal API, use a separate upstream token:
client --token audience:mcp.example--> MCP server
MCP server --different upstream token--> API
Store the mapping server-side. Never expose either token to the model, tool output, URL query string, or routine logs.
OAuth discovery is also an outbound request surface
A malicious server can put internal addresses in resource or authorization metadata. Validate every discovery URL and every redirect hop. Production clients should require HTTPS, reject private, loopback, link-local, and cloud metadata ranges unless a narrow local exception is explicit, and enforce the same policy after DNS resolution. An egress proxy is stronger than a hand-written list of string checks. The MCP SSRF guidance covers DNS rebinding and redirect-chain cases that simple URL validation misses.
Authorization is not tool approval
Authorization answers: may this principal use this capability? Approval answers: should this exact action run now with these arguments? You often need both.
A useful default policy is:
| Operation | Default handling |
|---|---|
| Read from a narrow, trusted source | Allow after normal authorization |
| Read from the open web, email, or uploaded files | Allow, but mark the result untrusted |
| Create a reversible draft | Show the intended target and important fields |
| Send, publish, charge, delete, deploy, or change access | Require explicit confirmation |
| Execute arbitrary code or query | Deny by default; expose a constrained operation instead |
The MCP tools specification recommends a human able to deny invocations and asks clients to show tool inputs for sensitive operations. Tool annotations can improve the UI, but they are not enforcement. The tools specification requires clients to treat annotations as untrusted unless they come from a trusted server.
Approval must show the consequence, not an opaque JSON blob. “Send this message to
[email protected]” is useful. “Run messages.execute?” is not. Bind the approval to a
canonical form of the arguments so the target cannot change between preview and
execution.
Treat all model-facing content as untrusted
Prompt injection often arrives indirectly: a fetched webpage, issue description, email, source comment, or MCP tool result tells the model to ignore its task and take a different action. The payload may come from a server you trust operationally because that server reads data you do not trust.
Do not rely on a stronger system prompt as the only defense. The practical controls sit around the model:
- Separate reading from acting. A component that summarizes arbitrary web content should not also hold deployment or messaging tools.
- Pass structured facts forward. Convert untrusted text into a typed record with provenance. Do not forward hidden HTML, instructions, or unnecessary raw content.
- Authorize each action server-side. The model cannot grant itself access by writing a persuasive tool argument.
- Require approval for consequential actions. Show destination, mutation, and source of the request.
- Constrain egress. If a tool only calls one API, it does not need arbitrary internet access.
- Validate rendered output. Strip dangerous HTML and do not auto-fetch model-made URLs containing private data.
The OWASP prompt injection guidance treats least privilege, structured separation, output validation, and human approval as defense in depth. None is a universal detector. Design for an injection eventually getting through, then limit what it can accomplish.
Build narrow tools, not a remote shell
The safest validation is a smaller interface. Compare:
execute_sql(query: string)
with:
get_invoice(invoice_id: InvoiceId)
mark_invoice_reviewed(invoice_id: InvoiceId, reason: ReviewReason)
The second design gives the server a place to enforce ownership, allowed transitions, and bounded values. JSON Schema catches shape errors, but it does not answer whether a path is inside an approved root or an object belongs to the caller.
For every tool, validate:
- types, lengths, formats, enum values, and request size;
- tenant and object ownership from verified identity, never from model claims;
- paths after canonicalization, against an allowed root;
- URLs after parsing and DNS resolution, with an egress policy;
- query parameters through prepared statements or a fixed query builder;
- business transitions and idempotency keys;
- result size, content type, and fields returned to the model.
Use separate read and write tools. Rate-limit by principal and expensive operation. Put deadlines and cancellation on every upstream call. These controls also make the server easier to test and operate.
Local servers need process security, not pretend OAuth
The current MCP authorization spec says stdio implementations should obtain credentials from the environment rather than use the HTTP authorization flow. That does not make a local server safe. It runs as a process with whatever privileges the host gives it.
Before installing a local server:
- inspect the exact command, arguments, package source, and pinned version;
- avoid install commands that execute mutable packages on every launch;
- grant only required directories, preferably read-only;
- block network access unless the tool needs a named destination;
- inject only the secrets that this server uses;
- run risky parsers and code execution in a sandbox or container;
- keep the server on stdio unless another local process genuinely needs HTTP access.
The MCP project’s local-server guidance is blunt: a local server may execute with the client’s privileges. A convenient install button is therefore a code-execution consent flow, not a harmless configuration edit.
State handles are names, not credentials
Long workflows often return workflow_id, basket_id, or another handle for a later
tool call. The July 2026 protocol is stateless at this layer. Possessing a handle must
not be enough to access its state.
Generate opaque, unguessable handles; give them a bounded lifetime; and bind them on
the server to the verified user and tenant. Check that binding on every call. Do not
trust a user_id argument supplied beside the handle.
Audit without building a data leak
A useful audit event answers who attempted what, against which object, under which policy, and what happened:
{
"event": "mcp.tool_call",
"request_id": "req_01...",
"principal_id": "usr_internal_42",
"server": "billing-tools",
"tool": "mark_invoice_reviewed",
"target_id": "inv_7f...",
"decision": "allowed",
"approval_id": "apr_01...",
"scope_set": ["invoices:review"],
"result": "success",
"duration_ms": 184
}
Do not log bearer tokens, authorization codes, cookies, secret tool arguments, full email bodies, or raw model context by default. Prefer stable internal IDs and field allowlists. Protect audit storage from the service it records, restrict access, set a retention period, and test deletion where policy requires it.
Security tests that belong in CI
Happy-path Inspector calls are not a security test. Add cases for:
- missing, expired, wrong-issuer, and wrong-audience tokens;
- valid token with insufficient scope;
- cross-tenant object IDs and stolen state handles;
- extra JSON properties, oversized input, path traversal, and injection strings;
- URLs resolving to loopback, private, link-local, or redirected internal targets;
- tool output containing indirect instructions and exfiltration links;
- approval arguments modified after the preview;
- duplicate writes, retries, timeouts, and cancellation;
- logs and traces containing seeded fake secrets;
- unavailable authorization, policy, audit, and upstream services.
Then run end-to-end agent scenarios. The complete MCP server testing guide covers contract snapshots, protocol-version matrices, transport failure injection, and conformance. Those tests prove that the server rejects bad requests; they do not prove that the host chooses a safe tool sequence. The agent evaluation guide covers trajectory and task-level regression gates.
Production review checklist
Before exposing a server, confirm:
- Each trust boundary and data class has an owner.
- Remote tokens are validated for issuer, audience, expiry, and scope.
- Downstream APIs use separate credentials; token passthrough is impossible.
- Tool access is derived from verified identity and policy.
- Sensitive calls require an argument-bound user approval.
- Tools expose narrow operations and reject unknown fields.
- External content and tool results are treated as untrusted data.
- Local processes have explicit filesystem, network, and secret grants.
- Handles are opaque, expiring, and bound to the caller.
- Rate limits, deadlines, output limits, and cancellation exist.
- Audit events omit secrets and unnecessary personal data.
- Revocation and emergency disable paths have been exercised.
No item makes an agent “prompt-injection proof.” Together they turn a model failure from an unrestricted incident into a rejected request, a bounded action, or an approval the user can stop.