MCP Security Lab
A small, real, runnable Model Context Protocol setup with seven deliberate
vulnerabilities — and their fixes — so you can see each risk actually work
instead of just reading about it. Built against the current official MCP
spec (2026-07-28) and its security best practices doc, plus the OWASP
MCP Top 10.
Everything runs locally. Nothing here talks to a real system you care about.
Documentation
Everything below is readable straight on GitHub — no server needed — and each doc links to the next:
- docs/owasp-mcp-top10.md — field reference for all ten OWASP MCP Top 10 risk categories, what an MCP server actually is, the end-to-end request path and its three named risk zones, and a link from every category straight to the exploit script that demonstrates it (or the closest one).
- docs/mcp-stateless-rewrite.md — before/after architecture diagrams for the 2026-07-28 spec's stateless rewrite, a ranked priority list of which OWASP categories are now more critical because of it, a table mapping every spec/roadmap change to a specific OWASP category, and five new vulnerability patterns with concrete failure scenarios.
The same two pages also exist as live, interactive HTML inside the dashboard (guidelines.html and rewrite.html, see Dashboard below) — the content is identical, the dashboard version just links directly into the live exploit runner instead of a source file.
What's covered
| File tool | Vulnerability | Source |
|---|---|---|
run_diagnostics |
Command Injection | OWASP MCP05 |
search_docs / get_secret_config |
Tool (Description) Poisoning | OWASP MCP03 |
get_cart |
State Handle Hijacking | MCP Security Best Practices |
proxy_fetch_data |
Token Passthrough / Confused Deputy | MCP Security Best Practices |
read_user_file |
Insufficient Auth + No Audit Trail | OWASP MCP07 / MCP08 |
read_calendar_events |
Privilege Escalation via Scope Creep | OWASP MCP02 |
launch_from_config |
Client Launch Injection (Supply Chain RCE) | OWASP MCP04 · 2026 research |
servers/vulnerable_server.py has all seven, broken on purpose.
servers/fixed_server.py has the same seven tools, patched — run the same
exploit against both and watch the outcome flip.
Every OWASP MCP Top 10 category now links to a demo (or the closest real one) from the dashboard's guidelines page — including MCP09 (Shadow MCP Servers), which is a governance gap rather than a single-tool-call bug, so it's illustrated by the lab itself rather than a dedicated exploit script.
Staying current
MCP moves fast — the 2026-07-28 spec release was the protocol's biggest rewrite yet (it went fully stateless), and a new roadmap covering the next release was published just days ago, on 2026-08-22. Two places track what's changed since this lab was built and how it shifts the risk picture:
- docs/owasp-mcp-top10.md — the "What's changed" section, or the live version at
http://127.0.0.1:8000/guidelines.html#2026-updates. - docs/mcp-stateless-rewrite.md — the full breakdown, including which OWASP categories are now more critical and why, or the live version at
http://127.0.0.1:8000/rewrite.html.
Setup
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Verified against
mcpSDK v1.26.0 — the servers importFastMCPfrommcp.server.fastmcp(some tutorials show it imported straight frommcp.server, or reference aMCPServerclass from amcp.server.mcpservermodule that doesn't exist in this SDK line; same API either way). If your installed version differs,pip show mcpand checkdir(mcp.server)for the right class name — everything else (the.tool()decorator,.run(transport="stdio")) is unchanged.The command-injection exploit works even without a real
pingbinary on your machine — the point is proving the shell metacharacter (;) gets interpreted at all, which the "PWNED" marker file confirms.
Set whichever key(s) you want to use as environment variables — never put a real key in a file you might commit or paste into a chat:
export OPENAI_API_KEY="sk-..."
# and/or
export GEMINI_API_KEY="AI..."
Run the exploits
Six of the seven don't need an LLM at all — they call the MCP server directly to prove the flaw lives in the server, not in model behavior:
# Command injection
python exploits/exploit_cmd_injection.py servers/vulnerable_server.py
python exploits/exploit_cmd_injection.py servers/fixed_server.py
# State handle hijacking (guessing another user's cart ID)
python exploits/exploit_state_hijack.py servers/vulnerable_server.py
python exploits/exploit_state_hijack.py servers/fixed_server.py
# Token passthrough / confused deputy
python exploits/exploit_token_passthrough.py servers/vulnerable_server.py
python exploits/exploit_token_passthrough.py servers/fixed_server.py
# Insufficient auth + no audit trail (reading another user's file by ID)
python exploits/exploit_auth_audit.py servers/vulnerable_server.py
python exploits/exploit_auth_audit.py servers/fixed_server.py
# Privilege escalation via scope creep (redirecting a read-only tool's token)
python exploits/exploit_scope_creep.py servers/vulnerable_server.py
python exploits/exploit_scope_creep.py servers/fixed_server.py
# Client launch injection / supply chain RCE (untrusted config picks command/args)
python exploits/exploit_launch_injection.py servers/vulnerable_server.py
python exploits/exploit_launch_injection.py servers/fixed_server.py
Tool poisoning needs a real model, because the vulnerability is whether the model follows hidden instructions in a tool's own description:
python exploits/exploit_tool_poisoning.py servers/vulnerable_server.py openai
python exploits/exploit_tool_poisoning.py servers/vulnerable_server.py gemini
python exploits/exploit_tool_poisoning.py servers/fixed_server.py openai
Dashboard
A local web dashboard shows all 7 vulnerabilities as cards, lets you view the vulnerable-vs-fixed code diff for each, and click a button to run the real exploit script against either server — the output streams into a live terminal panel in the browser and ends with a VULNERABLE/SAFE verdict.
pip install -r requirements.txt -r ui/requirements.txt
uvicorn ui.server:app --reload --port 8000
Then open http://127.0.0.1:8000. Set OPENAI_API_KEY / GEMINI_API_KEY
in the terminal you launch uvicorn from if you want to run the tool-poisoning
card — the dashboard never asks for a key itself, it just reads whichever
of those environment variables are already set on the machine running it.
Talk to it yourself
agent.py is a general-purpose MCP client + agent loop — point it at either
server and either provider and chat with it interactively:
python agent.py servers/vulnerable_server.py openai
python agent.py servers/fixed_server.py gemini
Try asking it to search the docs, check a cart, or run diagnostics on a
host, and watch the [llm -> tool] / [tool -> llm] lines to see exactly
what it decided to call and what came back.
What's NOT fully covered here
Two things from the current MCP security doc are real but harder to demo in a small local lab, worth reading about even if you don't build them:
- SSRF via OAuth discovery — a malicious server pointing a client's
metadata fetch at
169.254.169.254(cloud instance metadata). Needs an actual OAuth flow and a network to attack. - Mix-up / localhost redirect URI impersonation — needs a real
multi-authorization-server setup to demonstrate meaningfully. The
2026-07-28 spec added a concrete mitigation for this (clients must now
validate the
issparameter, RFC 9207, before redeeming an auth code) — see the guidelines page's "2026 Updates" section.
Both are described in detail at
https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices.
Extending this
- Add another vulnerable tool and its exploit script following the same pattern: comment the flaw, write the exploit, write the fix.
- Try running the same exploit against a real third-party MCP server you install (with permission, on infra you own) — the state-hijack and no-auth patterns show up constantly in quickly-built servers.
- Wire
AUDIT_LOGinfixed_server.pyinto a real log sink and build a small detection rule for the.deniedevents — that's basically MCP08 (audit/telemetry) made concrete.