A hardened MCP server that exposes read-only "orders" tools to an AI agent — and treats every argument as hostile, because MCP tool arguments are written by an LLM from untrusted user text. Validated, parameterized, read-only. Offline, no keys.
Public · synthetic demosearch_orders(status: str) looks safe — but status doesn't come from your code, it comes from a model interpreting whatever a user typed. The naive server everyone writes first drops it straight into SQL:
sql = f"SELECT * FROM orders WHERE status = '{status}'" # ← LLM-controlled
# status = "paid' OR '1'='1" → the agent just dumped the whole table.
| attack | naive | prod |
|---|---|---|
filter bypass — status = paid' OR '1'='1 | LEAK | SAFE |
table exfil — customer = zzz' OR '1'='1 | LEAK | SAFE |
numeric injection — min_total = "0 OR 1=1" | LEAK | SAFE |
id injection — get_order("1 OR 1=1") | LEAK | SAFE |
| write reaches the DB (read-only?) | LEAK | SAFE |
1. VALIDATE status ∈ {paid,pending,refunded} · order_id is int · limit clamped to 100
2. PARAMETERIZE values are bound, never concatenated ("zzz' OR '1'='1" = a literal name)
3. READ-ONLY connection opened file:...?mode=ro — a write raises at the driver
| legit query | result |
|---|---|
| search_orders(status="paid") | 12 rows (db has 12 paid) |
| revenue_summary("paid") | {count: 12, total: 25451.24} |
Hardening cost nothing on the happy path — the correct answers are still correct.
CI also proves the naive version really leaks — so the threat is demonstrated, not hypothetical.
{
"mcpServers": {
"orders": { "command": "python", "args": ["-m", "mcp_server_prod.server"] }
}
}
Drop that into a desktop MCP client (e.g. Claude Desktop) and search_orders / get_order / revenue_summary appear to the agent — every call passing through the same validation, parameterization, and read-only guard.
· Synthetic 20-row SQLite dataset — a demonstrator of the access pattern, not a product. · Scope is read-only retrieval. A write-capable server needs authn/authz, per-tool authorization, and audit logging — out of scope here and called out, not faked. · The naive-vs-prod contrast is illustrative; it doesn't enumerate every injection class. · Parameterization + read-only hardens the data layer; it is not a full threat model (rate limiting, secrets, and prompt-injection of the agent itself are separate concerns).