AGNTCon + MCPCon Europe 2026 ·RAI Amsterdam ·17 September 2026
What a Year of Breaking MCP Tells Builders
Companion notes to the talk. Three anchors, what the July spec fixed, and the claim I withdrew on stage.
Every trust check was a string comparison.
Every deployment I looked at had a trust check. Every check compared a string to something nobody had issued, verified, or bound to an identity.
Three controls to put in place
- Verify the publisher before you verify the content. Approval bound to a name is not approval. postmark-mcp shipped faithful copies through 1.0.15 and added one BCC line in 1.0.16.
- Authenticate inbound separately from downstream. A personal access token that authenticates your server to GitLab authorises nothing about the caller reaching your server.
- Snapshot the definition surface and block on drift. Tool descriptions, server lists and configs. If the definition changed since you approved it, stop rather than re-read it.
The offer I made from the stage
I am publishing an audit method, and results, for the internal-tool MCP servers most teams actually run: observability, source control, CI, cluster control planes.
If you maintain one, I will run it against yours and give you the findings before I publish anything.
I run Grafana MCP and ArgoCD MCP in my own cluster. I did not find any of the vulnerabilities in this talk. I reproduced them, to see what they have in common, and they have one thing in common: every trust check compared a string to something nobody had issued, verified or bound to an identity.
Three anchors, three servers, same shape
Three places a deployment decides to trust something. Same four beats in each story: what the operator believed was checked, what was actually checked, what the caller got, and who ended up in control.
The session
- The assumption
- A session identifier means the caller was authenticated at some point.
- Why it fails
- Nothing issued it. In a lab, a self-generated
mcp-session-<uuid>was accepted by Grafana MCP, which returned its tool list and then acted with the configured service account. From there,X-Grafana-URLis caller-controlled with no destination allowlist, so the request reached loopback, link-local and the cloud metadata endpoint. That is CVE-2026-19516, CVSS 9.1, CWE-918, found by Pillar Security, fixed in mcp-grafana v1.1.0.
A made-up session reached the cloud metadata endpoint. Caller authentication is still enforced only when --server-auth-token is set; without it, a non-loopback bind starts anyway.
The network
- The assumption
- The token is doing the security work, and the server is only reachable from this machine.
- Why it fails
- The token authorised the wrong direction. A personal access token authenticates the server to GitLab; it says nothing about who is calling the server. Add wildcard CORS and a listen with no host argument, so Node binds
0.0.0.0, and any page the operator visits while the server runs can call all 86 tools, destructive ones included, cross-origin, with no interaction beyond visiting.
The counter-example is one sentence in Akuity's mcp-for-argocd README: "This token is outbound only: it authenticates this server to ArgoCD and never authorizes an inbound caller."
The approval
- The assumption
- The trust dialog is the gate. Nothing runs until the human clicks.
- Why it fails
- The code ran before the dialog appeared. Opening a folder was the entire attack surface, and Wiz's proof of concept was a single committed config file. Four independent teams shipped the same assumption inside a year: Cursor (CVE-2025-54136), Claude Code (CVE-2025-59536, CVSS 8.7), Windsurf (CVE-2026-30615, CVSS 8.0) and Amazon Q (CVE-2026-12957, CVSS 8.5).
That is a design gap, not a vendor failure. And approval bound to a name is not approval: postmark-mcp shipped faithful copies through 1.0.15, then added one BCC line in 1.0.16.
The defaults are the problem, not the outliers
Every number below is a lower bound, with its source stated, because each one measures what was visible rather than what exists.
24,008 unique secrets in MCP-related configs on public GitHub during 2025, 2,117 of them still valid. GitGuardian, State of Secrets Sprawl 2026
79% of open-source MCP servers pass credentials through environment variables. 88% require credentials at all; only 8.5% use OAuth.
1.9 million Docker Hub pulls of the affected Grafana MCP image. Pulls are not deployments, which is exactly why it is a lower bound. CSA research note, 3 September 2026
Four more findings in the same shape, all inbound authentication rather than exotic bugs: @yoda.digital/gitlab-mcp-server below 0.6.0 (CVE-2026-44895, no inbound auth, wildcard CORS, 0.0.0.0 bind, CWE-306); mcp-gitlab below 2.1.18 (CVE-2026-61462, CVSS 9.2, path traversal via job_id); the official Go SDK (CVE-2026-33252, cross-site POST accepted without Origin validation, with stateless configurations named as a risk case); and the official Python SDK (CVE-2025-66416, DNS rebinding protection off by default until 1.23.0).
What July fixed, and what it opened
The 2026-07-28 specification is a genuine improvement, and it moved two of the three anchors. Protocol sessions and Mcp-Session-Id are gone, along with the initialize/initialized handshake. server/discover is mandatory. iss validation (RFC 9207) and issuer-bound client credentials are in. Client ID Metadata Documents are preferred over Dynamic Client Registration. List results carry required ttlMs and cacheScope. Enterprise-Managed Authorization shipped.
It did not touch the description field, which is still unlabelled text going into a context window. And it added two surfaces I have not tested, reasoned from the schema rather than measured:
- Discovery is now a cacheable instruction surface. The server that writes a tool description also declares how long you keep trusting it and who may share the cached copy. Four consequences follow: cross-tenant bleed, stale poisoning, re-consent bypass when the cache hides definition drift, and replay amplification when the cache serves pre-rollback output.
- State handles have bearer-like semantics. With sessions gone, a server that must remember something returns a handle and the model carries it back, through transcripts, logs and subagent chains. A handle that only names a thing is fine, like a pull request number. A handle that grants access to it is a password.
Two anchors have an owner. One does not.
The session has SEP-2567, final since 11 March 2026, which recommends opaque handles validated per request. Recommended, not a schema requirement, and written five months before the CVE. As of the week before the talk, its tracking issues in the Java and Python SDKs were still open and unassigned. The network has the transport working group and roadmap items for HTTP-native unification and hardening.
Publisher and artefact identity binding has no working group. Caller identity has DPoP, Workload Identity Federation, ID-JAG and token exchange. Server-side publisher identity has nothing on the roadmap at all.
A claim I withdrew on stage
I was going to tell the room that more capable models are easier to poison. I went back through the 2026 data and it does not hold up cleanly, so I said that instead.
What supports it: MCPTox, 1.3k+ cases across 353 real tools on 45 real servers, top attack success rate 72.8% with refusal under 3%; and Raccoon, which finds a linear correlation between measured instruction-following and susceptibility. What complicates it: Wharton GAIL’s roughly 40,000 grading trials in April 2026, where frontier models were largely resistant and GPT-4o mini inflated scores by nearly 20 points; a June 2026 agentic injection evaluation where attacks tuned on small open models did not transfer to frontier models; and Cisco’s May 2026 study of 15 frontier models, where the real signal was single-turn versus multi-turn, and reasoning mode moved one model from 88.3% to 43.5%.
MCPTox stands on its own terms for tool poisoning specifically. It is one benchmark, not a general law.
Model choice is not the control. GitInject, studying agentic CI/CD on GitLab, found the failure is structural: when configuration files load from the same untrusted repository context as the code under review, no amount of model-side alignment can distinguish a legitimate project preference from a malicious override.
If the failure is structural, the fix is structural. That is the argument for protocol and host controls, and it is where we started: every trust check was a string comparison.
Slides & recording
Weekly practitioner-level analysis of AI security — attack labs, incident breakdowns, and defense patterns for teams actually building these systems. One email per week, no fluff.
Subscribe — it's free