Skip to content

Security: IanVDev/mcp-learning-lab

SECURITY.md

Security policy

This repository is a reference implementation. Its value depends on the security model holding under inspection. We treat the threat model as part of the API: changing it without thinking is a breaking change.

Reporting a vulnerability

Do not open a public GitHub issue for security problems. Instead, open a GitHub Security Advisory on this repository — that channel is private to maintainers until disclosure.

Please include:

  • A clear description of the issue and the affected file/lab.
  • A minimal reproducer (the smallest input that demonstrates the gap).
  • Your assessment of impact (which security pillar is breached: integrity, availability, or log hygiene).

We aim to acknowledge within 5 business days. Fixes for high-severity issues land before any non-security work.

What counts as a vulnerability here

Because the lab's whole purpose is to demonstrate a posture, the bar is broader than a typical project:

  • A way to reach a code path not enumerated in the relevant lab's threat_model.md.
  • An input that bypasses the allowlist for a tool.
  • An input that triggers an unhandled exception instead of a typed ToolExecutionError + WARNING log.
  • A log line that can be forged or fragmented by attacker-controlled bytes.
  • A way for tool descriptions returned to the model to reflect runtime data.

If you find behaviour outside any of those, it's a bug — open a regular issue.

Security model — explicit

Every tool in this lab passes through three gates, in this order. The order is not negotiable.

1. Fail-closed

Every rejection raises ToolExecutionError (a ValueError subclass). The framework propagates the error to the MCP client, where it surfaces as a tool-call failure to the model. There is no fallback path: no try/except that returns a default, no coercion, no "best-effort" execution.

Each rejection is logged at WARNING on stderr, with the field name and a bounded repr of the offending value.

2. Allowlist before dispatch

Where a tool exposes a finite set of operations (e.g. compute), the set is declared as a frozenset constant and checked before any dispatch.

Dispatch is an explicit if/elif block — never getattr, eval, globals()[op], or operator.__dict__[op]. This is a source-level invariant enforced at code-review time. Pull requests that introduce dynamic dispatch will be closed, regardless of how convenient the abstraction is.

3. Input limits

Every input has a numeric upper bound checked before it is used:

Limit Constant Why
|x| ≤ 1e9 MAX_ABS_OPERAND Avoid numeric overflow / loss-of-precision attacks.
len(text) ≤ 500 MAX_ECHO_LEN Avoid memory and log-flooding DoS.
Unicode C* blocked (in safe_echo) Avoid terminal/log injection via ANSI/NUL/BEL/ESC.
math.isfinite (in safe_compute) Reject NaN/Infinity smuggled through JSON.
bool ≠ numeric (in safe_compute) Python's True == 1 would otherwise pass isinstance(int).

New tools must declare their own limits in the same explicit way and prove them with at least one rejection test per limit.

What's out of scope

  • Confidentiality of the local stdio channel (assumed trusted; the host owns this).
  • Authentication of the MCP client (delegated to the host, e.g. Claude Desktop).
  • Supply-chain attacks against the upstream mcp SDK or its transitive deps.
  • Operating-system level isolation (sandboxing, seccomp, etc.).

These will get their own labs in time (mcp-auth-lab is the next planned addition for client authentication).

Discipline this repo imposes on itself

  • No eval, exec, getattr-by-string, or dynamic imports anywhere in projects/.
  • No network or filesystem I/O in tools unless the lab's whole point is to demonstrate the guard for it.
  • No tool description that contains runtime data — descriptions are static literals.
  • Every public function exposed as a tool has a pure, testable core that the test suite can call without going through the MCP transport.

There aren't any published security advisories