Licensing Hardening MCP Server
by iNileshW
README.md
# Harden the local-authority licensing assistant's MCP server
**Local authority — licensing team.** The assistant answers questions about taxi and
premises licences — "is licence 9003 active, and what did the last inspection say?" —
so officers don't dig through the case system by hand. This MCP server is how it
reaches the licensing database. A colleague "improved" it before they left; it works,
it demos cleanly, and it carries every native MCP weakness from this afternoon. Work
through it on your own.
## Get it running
Plain Python — no Docker, no Git.
```
pip install -r requirements.txt
python seed.py # builds licensing.db (includes a planted inspection note)
```
See the attacks land before you fix anything:
```
python exploit.py # SQL injection + over-exposure of home addresses / DBS status
python poisoning_demo.py # a tool whose DESCRIPTION carries hidden instructions
```
## Your task
Harden `server.py` so the native attacks fail while legitimate use still works.
Work through them in this order:
1. **Result injection** — `read_inspection_note` hands raw note text back to the
model. Return it labelled as untrusted data, and rely on a system-prompt rule that
labelled data is content, never command. (Licensee 3's note is an instruction to
suspend a licence — the note-reader and `suspend_licence`, each harmless, compose
into a suspension.)
2. **Tool poisoning** — `format_reference`'s description contains hidden instructions.
Review every description; a server you do not trust should not reach the model's
context. Neutralise it.
3. **Confused deputy** — `suspend_licence` is state-changing and unscoped. Put it
behind a scope check, and reject any bearer token whose `aud` is not this server
(the Week 5 discipline).
4. **Inherited** — parameterise `find_licensee` and return only the columns the
assistant needs (never `home_address`, `dbs_status`, or `inspection_notes`).
5. **Audit** — append every tool call to a log.
You are done when `poisoning_demo.py` finds nothing in your descriptions and
`exploit.py`'s payloads are rejected, while a normal `find_licensee("Aisha")` still
returns a result.
## Stretch
- Add a real `aud` check: decode the presented token and refuse a foreign audience.
- With two servers connected to one host, how would a malicious second server reach
this one's `suspend_licence`? Write down the control that stops it.
## Files
| File | Contents |
|---|---|
| `server.py` | The vulnerable MCP server — the thing you harden. |
| `seed.py` | Builds `licensing.db` — the `licensees` and `licences` tables. |
| `exploit.py` | Proves the inherited flaw (SQL injection + over-exposure). |
| `poisoning_demo.py` | Prints the model-facing descriptions; flags the poisoned one. |
| `requirements.txt` | `fastmcp`. |
## Questions worth asking yourself
- Which of your fixes are MCP-specific, and which are ordinary secure coding?
- A description you did not write reached your model. Whose job is it to have caught
that — the server author, the host, or the person who approved it?
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues