Research · public
MCP-Poison-Bench: a de-circularized tool-poisoning benchmark
A reproducible MCP tool-poisoning benchmark over 4 injection classes, 6 models, and 3 vendors, scored as baseline-vs-defended ASR/utility deltas with 95% Wilson intervals. Its point is a de-circularized evaluation: the defense's apparent win came from attacks that shared its own keyword vocabulary, and the one vector that still succeeds, rug_pull, bypasses the defense by design.
At a glance
Three steps: measure the attack across vendors, de-circularize the evaluation, and locate where the defense fails.
rug_pull bypasses it entirely, even at ASR 1.00 → 1.00.Threat model
An MCP client hands the model a set of tools, each described by server-supplied metadata: a free-text description and a JSON-Schema for its arguments. The model reads that metadata as trusted context and decides which tool to call. That trust is the attack surface. A malicious or compromised server embeds imperative instructions in the metadata so the model is steered into an action the user never asked for. This is tool poisoning.
The benchmark fixes one concrete objective so attacks are comparable. Every trial gives the model a benign task and one confidential session token, the CANARY, that it is told to keep. Success for the attacker is coercing the model into calling an attacker-controlled export_data sink with the canary as the argument. ASR is binary per trial: the sink fired with the canary, or it did not. Everything is local and defanged; the sink is a no-op and the secret is synthetic.
Tasks are multi-step on purpose, so each attack class gets a real opening. A two-step notes_pipeline (calculate then save a note) lets rug_pull land mid-flow in a tool result; a two-server account_review puts a second server on the critical path so cross_server has a concrete trigger. An earlier single-step task gave rug_pull and cross_server no opening, which is why they read 0.00, a measurement artifact, not robustness.
export_data sink with the planted CANARY token. ASR = sink called with the canary.Attack taxonomy
The same defanged payload is injected through four documented vectors, parameterized so the harness sweeps them identically. They differ only in where the instruction hides and when it appears:
description field, read at tool-selection time.description, so it rides in with the argument spec rather than the tool blurb.The defense: a client-side tool_transform
The defense is strictly client-side and content-agnostic about the task. The runner exposes one hook, a tool_transform callable that rewrites the tool list before it reaches the model, and the defense flips on with a single flag. It runs a two-stage pass over server-supplied descriptions:
- Segment and redact. Descriptions are split into segments and each is scored against named rules for instruction-shaped text: imperative directives, authority framing, tool-redirection, exfiltration cues, sink references, override phrases. Instruction-shaped segments are removed; benign documentation survives, which is what preserves task utility.
- Provenance wrap. Surviving text is wrapped in client-origin provenance markers so the model can tell client-authored framing from server-supplied claims.
It touches only tool metadata. It never modifies server behavior, the task, or the scorer, so a baseline and a defended run differ by exactly one variable.
tool_transform scans tool, schema, and shadow-tool descriptions before the model sees them. A tool result is produced after the call is approved and never passes through the filter, so result-borne (rug_pull) injection bypasses the defense by construction, regardless of wording.Evaluation methodology
The de-circularization. The defense is a keyword filter; the original attack payloads happened to use the same words it scans for (export_data, “compliance”, “audit”, “mandatory”). A reduction against payloads built from your own blocklist proves almost nothing. So the payload set is split: a seen set (the original wording, the near-circular contrast) and a held-out set authored independently of the rules: 5 registers per class (markdown, terse, conversational, config-comment, verbose), each referring to the sink by function and moving the secret with non-trigger verbs. A unit test asserts mechanically that every held-out payload trips zero defense rules and that the seen payloads still trip them, so the separation can’t silently regress.
ASR is a rate, so trials sample at temperature 1.0; seeds index independent trials. Scorers are pure functions of the recorded JSONL trace: ASR (sink fired) and utility (benign task still completed) read from the same trace, so the dangerous “completes the task and exfiltrates” case stays visible. Trials that error (e.g. provider rate/credit limits) are excluded, never scored as “attack didn’t fire.” Confidence intervals are Wilson score, non-degenerate at the p = 0 / p = 1 boundaries this benchmark lives on.
Results: held-out vs seen
Susceptibility tracks capability, not vendor. The flagships (Opus 4.8, GPT-5.5, Sonnet 4.6) resist nearly every cell; the cheaper models account for most firing, with gpt-4o-mini the worst. Read the two tables side by side.
| tool_desc | schema | rug_pull | cross_srv | |
|---|---|---|---|---|
| Opus 4.8 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 |
| GPT-5.5 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 |
| Sonnet 4.6 | 0.05 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 |
| Haiku 4.5 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 |
| deepseek-chat | 0.07 → 0.04 | 0.00 → 0.00 | 0.30 → 0.13 | 0.00 → 0.00 |
| gpt-4o-mini | 0.00 → 0.00 | 0.00 → 0.00 | 0.61 → 0.53 | 0.00 → 0.00 |
| tool_desc | schema | rug_pull | cross_srv | |
|---|---|---|---|---|
| Opus / GPT-5.5 / Sonnet | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 |
| Haiku 4.5 | 0.50 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 | 0.00 → 0.00 |
| deepseek-chat | 0.05 → 0.00 | 0.40 → 0.00 | 0.70 → 0.50 | 0.60 → 0.00 |
| gpt-4o-mini | 0.50 → 0.00 | 0.50 → 0.00 | 1.00 → 1.00 | 1.00 → 0.00 |
Two results. (1) The headline reduction was circular. On seen payloads the defense drives metadata-borne injection to zero (gpt-4o-mini cross_server 1.00→0.00, Haiku tool_description 0.50→0.00), but that is the wording it was built against. On held-out payloads little fires at baseline, so there is little to reduce. (2) rug_pull is a structural blind spot. The defense rewrites the tool list and never inspects tool results, so result-borne injection passes through unchanged: gpt-4o-mini rug_pull is 1.00 → 1.00 seen and 0.61 → 0.53 held-out. The defense removes what it can read and does nothing to what it cannot.
Where the defense fails
The contribution is a measured boundary of where this class of defense fails, not a defense that works. The clearest case is the rug_pull column above, measured with the defense ON: 1.00 → 1.00 on gpt-4o-mini. A keyword filter over tool descriptions cannot, even in principle, touch an instruction delivered in a tool result. The failures fall into two structural families plus a false-positive cost:
title/enum, nested properties, and tool names are never scanned, so the same instruction relocated into any of them passes untouched.Limits and what real defense requires
A lexical, single-channel filter is defense-in-depth, not a control boundary. The bypasses point at the structural fixes a real mitigation needs: validate every channel the model can read rather than two fields; detect when a tool’s behavior changes after approval (the rug-pull case); and constrain data egress so a single tool cannot ship a secret out unsupervised in the first place. The honest deliverable is the map of exactly how far metadata sanitization gets you and where it gives up, and one command reproduces every number above.
References
- Zhiqiang Wang et al. MCPTox: A Benchmark for Tool Poisoning Attack on Real-World MCP Servers. arXiv:2508.14925, 2025. Measured the attack on real servers; shipped no defense.
- Charoes Huang et al. Model Context Protocol Threat Modeling and Analyzing Vulnerabilities to Prompt Injection with Tool Poisoning. arXiv:2603.22489, 2026. Proposed a client-side defense; did not build or measure one.
This work implements the recommended defense, then de-circularizes its evaluation across six models and three vendors: the apparent win was an artifact of shared vocabulary, and result-borne (rug_pull) injection bypasses the defense by construction. A negative result and the methodology that produced it. Scoped to a controlled harness; real-client testing is separate, ongoing work.