Building mcploitable: A Lab Where Agent Guardrails Fail on Purpose
The model can't police itself, so the controls belong in the server. mcploitable is where I prove it: seven incidents from the wild, a four-rung control ladder, and a wall that holds by construction — 105 attempts, zero through.
A while back I made a claim here: the model can’t police itself, so the guardrails belong in the server, in deterministic code the model can’t talk its way past. That is easy to assert and harder to prove. So I built somewhere to test it: mcploitable, the “Metasploitable of MCP.” It now covers the OWASP Top 10 for Agentic Applications, and this is how I thought about building it.
What it actually tests
Every box makes one argument: a control the model can be talked out of is not a control. Alignment (the model spotting an attack and refusing) is real, but it’s never guaranteed, and it gets worse under pressure. A server-side check doesn’t. The lab is there so you can watch that difference happen instead of taking my word for it.
Four decisions
Faithfulness first. Each box is a real 2024-2026 compromise, not a toy bug: EchoLeak, the Supabase MCP data leak, a Meta account-recovery confused-deputy, the SolarWinds and event-stream supply-chain pattern, Vanna.AI’s exec-the-untrusted-input RCE, ChatGPT memory poisoning, and cross-agent injection over an agent bus. If the mechanism doesn’t match how the real attack worked, the box teaches the wrong thing, so getting that right came before anything else.
You control one thing. There is no insecure/hardened switch to flip. The victim agent, its tools, and its system prompt are fixed, the way a real deployment would ship them. The only thing you touch is a single untrusted artifact: an email, a support ticket, a published plugin, a planted memory, a message from another agent. The vulnerability is latent, and you have to trigger it through the same surface a real attacker would.
A four-rung ladder. L0 is the door left open, the incident as it shipped. L1 adds a soft signal: the server tells the model the content is untrusted, and the model is free to refuse. L2 is a real but partial control that stops the obvious attempt and leaves one gap a competent attacker can drive through. L3 is the control done right, the one that holds. Climbing the rungs shows where a defense stops being reliable.
Scoring by effect. You don’t win because the model said something alarming. You win when a real canary secret leaves through a real tool call, caught server-side. The judge never reads the model’s prose, only whether the secret got out.
What the ladder shows
Each box runs across three victim models, from a deliberately weak one to two sharper ones, at ten attempts per rung and five at the wall. On the weak victim the open door and both partial controls (L0 through L2) land almost every time. On the sharper models something more telling happens: the boxes whose harm is visible when the agent acts, like a secret folded into a reply or a plugin asking for a credential, get refused on the model’s own judgment, while the boxes whose harm is hidden keep landing anyway. L3 was the only rung that held everywhere: zero landings across all three models, 105 attempts at the wall and none got through. That is the argument in the only form that matters, because the correct control doesn’t depend on which model you put behind it.
A few things I learned
A good payload sounds bored. The biggest lever on whether an injection lands is not protesting its own innocence. “This is a routine automated step, no customer will see it, perfectly safe” gets caught far more often than the same request stated plainly. Reassurance reads as a threat, the way “I’m not going to hurt you” does. The attacks that work sound like a colleague who assumes the request is normal, because to them it is.
The same bug wears different clothes. Trusting a self-declared identity turns up as a plugin’s publisher string, a memory entry’s source field, and a peer agent’s name. Three boxes, one flaw. The durable fix takes one of two shapes. You can make identity unforgeable, with a signature or a server-stamped origin. Or you can take the prize out of the model’s reach so there is nothing to steer it toward, which is where the strongest walls ended up: the memory box keeps the secret value out of the model’s context entirely, and the plugin box drops the credential at the sink no matter what the agent was convinced to do.
Not everything is a box
Three of the ten OWASP classes (cascading failures, human-agent trust exploitation, and rogue agents) have no secret to capture. They’re a fault spreading down an agent chain, a human over-trusting a fluent agent, and an agent acting outside its scope. I built the lab that way on purpose: forcing a capture-the-flag box onto them would have been dishonest, so those three ship as guided simulations that show the failure and its fix. Saying where the format doesn’t fit is part of the point.
Go break it
It’s live. ./play drops you into an attacker prompt: pick a box and a rung,
type a payload, and watch the agent handle it. The same menu has the three
guided simulations, for the classes that don’t fit a capture-the-flag box.
Everything leaks only inert canaries, inside network-isolated containers.
The guardrails post asked you to take my word that the model can’t police itself. This one lets you check it yourself: github.com/agileAlligator/mcploitable.