Skip to main content
Glama
ibrahimsaleem

MCP Security Lab

README.md
# 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](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](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](#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](docs/owasp-mcp-top10.md#whats-changed-since-this-reference-was-written) — 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](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

```bash
python -m venv .venv
source .venv/bin/activate         # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```

> Verified against `mcp` SDK v1.26.0 — the servers import `FastMCP` from
> `mcp.server.fastmcp` (some tutorials show it imported straight from
> `mcp.server`, or reference a `MCPServer` class from a `mcp.server.mcpserver`
> module that doesn't exist in this SDK line; same API either way). If your
> installed version differs, `pip show mcp` and check `dir(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 `ping` binary
> 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:

```bash
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:

```bash
# 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:

```bash
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.

```bash
pip install -r requirements.txt -r ui/requirements.txt
uvicorn ui.server:app --reload --port 8000
```

Then open `http://127.0.0.1:8000`. To run the tool-poisoning card you need
an OpenAI or Gemini key — either set `OPENAI_API_KEY` / `GEMINI_API_KEY` in
the terminal you launch uvicorn from (convenient for local single-user
use), or paste a key directly into the dashboard's API key field when you
run that card. A pasted key is bring-your-own-key: it's sent only for that
one run, injected only into that one exploit subprocess's environment, and
never stored, logged, or written to disk — this is what makes it safe to
run this dashboard for more than one person (e.g. a public deployment)
without everyone spending the same API budget.

## Deploying (Cloud Run)

The dashboard needs a real Python process — Firebase Hosting alone is
static-file-only and can't run it. **Firebase Hosting also can't proxy
WebSockets to Cloud Run**, and this app's live exploit stream is a
WebSocket, so it's deployed straight to Cloud Run and used on its own
`*.run.app` URL rather than fronted by Firebase Hosting.

Requires the `gcloud` CLI, authenticated, with a GCP project selected
(`gcloud auth login`, `gcloud config set project YOUR_PROJECT_ID`):

```bash
gcloud run deploy mcp-security-lab \
  --source . \
  --region us-central1 \
  --allow-unauthenticated \
  --port 8080
```

`--source .` builds the `Dockerfile` in this repo via Cloud Build and
deploys it — no separate `docker build`/`push` step needed. Once it's up,
`gcloud run deploy` prints the service URL; that's the dashboard.

Because this app intentionally runs real command injection and
subprocess-launch exploits, **don't set `OPENAI_API_KEY` / `GEMINI_API_KEY`
as env vars on the Cloud Run service for a public deployment** — leave them
unset so every visitor has to paste their own key (see [Dashboard](#dashboard)
above) rather than spending yours. The deterministic (non-LLM) exploits need
no key at all and only touch a sandboxed subprocess inside the container, so
no additional auth wall is required for those.

## 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:

```bash
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 `iss` parameter, 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_LOG` in `fixed_server.py` into a real log sink and build a
  small detection rule for the `.denied` events — that's basically MCP08
  (audit/telemetry) made concrete.