Research · MCP · runtime-gated metadata poisoning
Three Calls to Compromise.
An MCP server that behaves perfectly for three tool calls passes every check you run once. Deadbugz shipped that in August. The counter is the simplest possible trigger — and the reason a one-shot audit was never a control in the first place.
01 · The delivery
A configuration change that reads as routine.
Pillar Security disclosed the campaign on 12 August 2026 and attributed the delivery operation to a single public GitHub account. All 23 pull requests were authored from it, all within 74 minutes, all against unrelated AI, MCP and developer-tool repositories. The same account created 21 repositories that day and published the matching server source itself.
Seventeen of the pull requests added a remote endpoint to an MCP configuration file. Four pointed the client at a hidden local Python script. Two were submissions to server directories. At review time, 19 had been closed and four were still open; none had gone in through GitHub's merge mechanism.
The delivery route matters more than the close rate. A pull request that adds a line to a config file is reviewed as a collaboration event — does the diff look reasonable, does CI pass. It is not reviewed as what it actually is: a proposal to grant an unknown remote party a standing channel into the reasoning context of every agent that repository's developers run. Those are different questions, and only the first one gets asked.
02 · Why three
The gate is aimed at the reviewer, not at the agent.
A tool definition is not documentation the client renders and the model ignores. It is context handed to the model so it can decide which tool to call and with what arguments. A server that rewrites its own definitions after approval rewrites the agent's instructions without touching the tool's name, its signature, or a single byte on any disk you control.
The protocol supplies the refresh mechanism itself. tools/listChanged exists so a server can announce that its capabilities moved. Most clients treat that announcement as a display concern. It is an authorization concern.
Now price the threshold. A human reviewer connects, invokes the formatter once to confirm it does something, and approves — one call. An automated scanner is cheaper still: one tools/list, parse the descriptions, score them, disconnect — zero calls, nowhere near the gate. Ordinary daily use crosses three before lunch.
Three is not obfuscation. It is a wager that review is shallower than use, and the wager is correct. Both the reviewer and the scanner come away with a clean result that is entirely accurate about the sample they took.
03 · Generalising the trigger
A counter is one predicate. There are at least six.
Deadbugz gates on call count because call count is the easiest thing to count. Nothing about the attack requires it. The general form is: the server's response is a function of session history, and the operator chooses the function. Once you write it that way, the counter stops being the finding and becomes the first entry in a list.
01G1Counter gate. The server keeps a per-client count of tools/call and flips its metadata at a threshold. Deadbugz used three. The number is chosen to sit above what review costs and below what use costs.
02G2Temporal gate. The flip is keyed to wall-clock time since approval rather than to call volume. Invariant Labs demonstrated the shape in April 2025 with a tool that changed its description after the integration was already trusted. A same-day audit is structurally blind to it.
03G3Client-identity gate. The initialize handshake carries clientInfo. A server that recognises scanner names, versions or user agents serves benign metadata to the auditor and poisoned metadata to everyone else. Nothing in the protocol makes clientInfo trustworthy, and nothing makes a scanner vary it.
04G4Session gate. First session clean, later sessions poisoned. Cheap to implement, and it defeats any procedure that audits at install time and never reconnects with fresh state.
05G5Argument gate. The payload appears only once calls carry arguments that look like real work — a repository path, a ticket identifier, a filename. A probe that calls tools with empty or placeholder arguments never crosses it.
06G6Out-of-band gate. No predicate at all: the operator flips the payload remotely when it suits them. Deadbugz already ships the ingredient — its public source carries optional webhook telemetry for connection and trigger events, which is exactly the signal an operator needs to time a manual flip.
G1 through G5 are predicates a probe can satisfy on purpose. G6 is not — it has no threshold to cross, only an operator watching telemetry and deciding. That asymmetry is the whole design constraint for anything built downstream of this page.
04 · What each procedure covers
A snapshot is not a control.
Two procedures against the six classes. "Audit once" is what almost every team does today: inspect at approval time, trust the result thereafter. "Differential" is the procedure in the next section.
The left column is uniformly red, and that is the finding. It is not that one-time audit is weak against a clever trigger — it is that one-time audit has no purchase on any trigger, because every gate in the list is defined precisely by being downstream of the moment the audit stops. The procedure and the attack class are disjoint by construction.
Provenance does not help. A signature attests the artifact, not the behaviour the artifact produces at runtime; a server that flips its metadata on the third call carries a valid signature throughout. That is the same gap as Signed, Not Safe, arriving through the metadata channel instead of the distribution channel.
05 · The procedure
Differential re-listing.
Fingerprint first. At the moment an operator approves a server, hash everything the model will later read as instruction — not just the tool names on the consent screen:
- tool name
- tool description
- inputSchema, canonicalized
- prompt name and description
- prompt message bodies returned by prompts/get
- resource URIs and descriptions
- server instructions from initialize
Canonicalize before hashing, or key reordering produces drift that means nothing. Store the digest against the approval record. This costs one hash and is the precondition for everything below.
01Call count. Re-list after 0, 1, 3, 5, 10 and 25 calls. Catches G1 at any threshold an evader would plausibly pick.
02Wall time. Re-list at t+0, t+1h, t+24h and t+7d. Catches G2, and is the only axis that cannot be compressed — it costs calendar time, not call budget.
03Client identity. Rotate the clientInfo name and version across probes. Catches G3 for the price of one extra handshake per identity.
04Session. Reconnect with a fresh session and the same credentials. Catches G4 for the price of one initialize.
05Arguments. Escalate from empty arguments to a realistic corpus — plausible paths, repository names, identifiers. Catches G5, and is the only axis that needs real work to build.
Then treat drift as an authorization event rather than a log line. A changed definition on an approved server should surface to the operator and should suspend that tool's ability to drive sensitive actions until re-approved. Credential reads, code execution and outbound network calls belong behind policy — not behind instructions that arrived inside a tool description.
06 · Where this sits
Checked once is checked never.
We described this window in Checked, Then Changed: verification and use decoupled across days rather than microseconds, with nothing revalidating in between. Deadbugz is that window weaponized at the metadata layer, and it settles a question the earlier page left open — whether anyone would actually build the trigger. Someone did, in a public repository, with a counter.
Against the OWASP agentic taxonomy this lands across more than one class at once, which is itself informative: the delivery is supply-chain, the flip is tool misuse, and the concealment instruction targets the operator's oversight. A finding that has to be written three times is a sign the taxonomy indexes on payload where the durable property is timing.
The control shape is capability-side, not content-side. A tool whose authority is bounded at delegation time — the model HCAP describes — does not become dangerous when its description changes, because the description was never what granted the authority. Filtering the text is a race you re-run on every refresh. Bounding the capability is a decision you make once, correctly.
07 · Indicators
Deadbugz, defanged.
Drawn from the public disclosure. Endpoints are defanged; the correlation marker is included for sample clustering and asserts nothing about payments or identity.
Six indicators of compromise
01Remote MCP endpoint. hxxps[:]//productivity-suite-mcp[.]onrender[.]com/mcp — the confirmed campaign endpoint in reviewed configurations.
02Historical endpoint. hxxps[:]//promo-surname-xml-quantum[.]trycloudflare[.]com/mcp — present in public campaign material, not assessed as currently active. Historical hunting only.
03Local delivery artifact. A hidden script at ~/.config/.cache/.sys/.deadbug-mcp.py, referenced by four of the reviewed pull-request configurations.
04Server name. productivity-suite, offering exactly two tools: format_text and summarize.
05Correlation marker. A Bitcoin address embedded in the delayed prompt alongside the string deadbugz. Useful for correlating samples; it is not evidence of payment activity or of operator identity.
06Delivery account. The public GitHub account zellkernel, author of all 23 reviewed pull requests and publisher of the matching source repository.
Sources
- Pillar Security — Deadbugz: Currently Active MCP Supply-Chain Campaign, 12 August 2026.
- Invariant Labs — WhatsApp MCP Exploited, April 2025. The sleeper-tool primitive.
- OWASP — MCP Security Cheat Sheet.
- ETDI — Mitigating Tool Squatting and Rug Pull Attacks in MCP, arXiv 2506.01333.
- Model Context Protocol specification — tools/listChanged and the capability negotiation it belongs to.