The official security best practices name specific attacks, each with conditions that make it possible and a rule that stops it. Learn them as pairs: the setup, and the one control that breaks it.
A team builds an MCP server that proxies to a third-party API. To keep it simple, the proxy uses one static OAuth client ID with that API, and lets MCP clients register dynamically.
Alice uses it legitimately once and approves access. The third-party authorization server sets a consent cookie in her browser.
An attacker registers their own MCP client with the proxy, using a redirect URI they control, and sends Alice a link. Alice clicks. The third-party server sees the cookie, skips the consent screen ("she already approved this client ID"), and the authorization code flows through the proxy to the attacker. The proxy was the confused deputy: it lent its trusted identity to someone Alice never approved.
The fix is one rule: the proxy MUST show its own consent screen for each MCP client (naming the client, the scopes and the redirect URI) before starting the third-party flow.
MCP proxy with a static client ID at a third-party authorization server, dynamic client registration, a consent cookie, no per-client consent
Proxies MUST implement per-client consent (show client, scopes, redirect URI; CSRF-protected; not frameable), exact redirect-URI matching, single-use state stored only after consent
Token passthrough
Server accepts tokens not issued for it and forwards them downstream
Servers MUST NOT accept tokens not explicitly issued for them; use a separate upstream token
SSRF
A malicious server puts internal URLs (cloud metadata 169.254.169.254, localhost, private IPs) in resource_metadata or authorization server URLs
Server-side clients MUST consider SSRF: require HTTPS, block private and link-local ranges, validate redirects, pin DNS, consider an egress proxy
State handle hijacking
Attacker obtains or guesses a cart, task or job handle
Servers MUST verify every request and MUST NOT treat possession of a handle as authentication; bind handles to the user from the token
Local server compromise
One-click config with a malicious startup command; DNS rebinding to a localhost server
Clients MUST show the exact, untruncated command, flag it, require explicit approval; SHOULD sandbox. Servers SHOULD use stdio or authenticate local HTTP
Malicious authorization URL
A server returns javascript:, data: or file: URLs, or the client opens URLs via a shell
Allow only http(s) (http for loopback in development); MUST reject other schemes; MUST NOT open URLs with shell commands
Mix-up
A malicious authorization server obtains a code issued by an honest one
Validate iss (RFC 9207). PKCE alone does not prevent it
Plus scope minimization: request a minimal scope first and elevate with WWW-Authenticate challenges; avoid wildcard scopes and publishing every scope up front.
Every one of these happens at a seam: between your server and someone else's authorization server, between a host and a URL, between a handle and a user. The exam tests whether you can spot which seam is broken from a short scenario, so learn the preconditions, not just the names.
// refuse: not HTTPS, and 169.254.0.0/16 is link-local (cloud metadata)blocked: resource_metadata must be https and must not resolve to a private, loopback or link-local address
Illustrative. The same checks apply to authorization server URLs and redirect targets.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs # http://localhost:3000/mcp, logs appear here# 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() { # usage: mcp <method> '<json body>' [extra curl args...] curl -sS -N "$MCP" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -H 'MCP-Protocol-Version: 2026-07-28' \ -H "Authorization: Bearer ${MCP_USER:-alice}" \ -H "Mcp-Method: $1" "${@:3}" -d "$2" \ -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
Most of these are client- or proxy-side checks. Two you can run now against the reference server, plus a checklist for your own code.
ShellTests
# 1. State handle hijacking: Bob replays Alice's cart handle (see track 1, sessions)CART=$(mcp tools/call '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"create_cart","arguments":{},'"$META"'}}' -H 'Mcp-Name: create_cart' | sed -n 's/.*"cartId":"\([^"]*\)".*/\1/p')MCP_USER=bob mcp tools/call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add_to_cart","arguments":{"cartId":"'"$CART"'","sku":"TV"},'"$META"'}}' -H 'Mcp-Name: add_to_cart'# expect: isError true, "Unknown cart" (possession of the handle is not authentication)# 2. Token passthrough's cousin: a token minted for another server is refusedcurl -sS -i http://localhost:3000/secure/mcp -X POST -H 'Authorization: Bearer token-other' -H 'Content-Type: application/json' -d '{}' | head -1# expect: 401# 3. Checklist for your own client and proxy:# - does it refuse resource_metadata/AS URLs that are http, private or link-local?# - does it show the exact install command and require approval?# - does it reject javascript:, data:, file: authorization URLs, and avoid shell-open?# - does your proxy show its own consent screen per client before the upstream flow?