Agents & Autonomy 19 September 2026 7 min read 1,505 words

The tab is already a client

Four MCP servers were found with the same flaw in eight days, two of them scored Critical. In none of the attacks does anyone say anything to the model. The agent security that is failing right now is not about the model at all.

The argument

A local MCP server's most likely attacker is a browser tab rather than a poisoned prompt, and the specification's MUST for Origin validation does not prevent it because nothing in the stack fails when a server omits it.

On 11 September the GitHub Advisory Database published CVE-2026-59971 against mysql-mcp-server, scored CVSS 10.0. The reporters had gone looking and found twenty-five publicly reachable instances. Four days later came CVE-2026-61568 against @zereight/mcp-gitlab at 9.6. Three days after that, process-compose. This morning someone opened issue 35791 against Backstage saying its MCP actions backend has it too.

Four projects, eight days, one bug. And in none of the attacks does anyone say a word to the model.

That is the part worth stopping on. The agent safety conversation is almost entirely about the model: whether it can be tricked, whether it will obey an instruction hidden in a web page, whether the transcript can be poisoned. Those are real problems. They are not these problems. The vulnerability shipping Critical scores this fortnight is that the tools we built so the model could act are, considered as software, unauthenticated HTTP services running on a developer's laptop, and their most likely attacker is a browser tab. The specification says servers MUST validate the Origin header. Nothing in the stack fails when they do not.

How a web page calls your local tools

The mechanism is older than any of this and worth learning properly, because the intuition it breaks is one almost everybody has.

Start with the setup. An MCP server exposes tools to an agent over a transport. If that transport is stdio, the server is a child process reading a pipe, and nothing on the network can reach it. But teams move to Streamable HTTP or SSE for good reasons: several clients, a remote host, a server that outlives the session. Now the server is listening on a port. Bind it to 127.0.0.1 and you have done the obvious thing, and you have probably assumed you are finished.

You are not, because your browser is on 127.0.0.1 too. A page you visit can issue requests to localhost. The only thing that normally stops that page reading the response is the same-origin policy: the browser will send the request, but it will refuse to hand evil.example the bytes that came back from a different origin.

DNS rebinding removes that refusal. The attacker controls evil.example and its DNS, with a time-to-live of a second or two. You load the page; the name resolves to the attacker's real server; the script starts. A moment later the script fetches from the same hostname again, and this time the attacker's DNS answers 127.0.0.1. The browser checks origins by name, not by address. As far as it is concerned this is still evil.example talking to evil.example, so it is same-origin, so the script may read the response. The connection, meanwhile, is going to the MCP server on your machine.

The browser has become a proxy into your loopback interface, and it is an authenticated one: whatever credentials that server holds, it uses. In the mcp-gitlab case it holds a live GitLab token, and the advisory describes the attacker enumerating tools and calling the GitLab API through it, CI/CD variables included. In the MySQL case the server passed queries straight to cursor.execute() with no authentication on any route, so the advisory's summary is simply "full data dump." It also bound to 0.0.0.0 by default, which is how twenty-five of them ended up directly on the internet without needing the browser trick at all.

You do not need the server to hold a credential for this to hurt. process-compose manages local processes, and with its control tools exposed an unauthenticated caller can enumerate process state, start and stop and scale things, and read the logs. The advisory is blunt about what that yields: service names, local paths, usernames, internal URLs, secrets. A tool that only reads is still a tool that reads on your behalf.

The defence is not IP filtering, because the request genuinely comes from localhost. The classification GitHub assigned the GitLab case names the mistake exactly: CWE-350, reliance on reverse DNS resolution for a security-critical action. The fix is the Origin header, which the browser sets honestly and a script cannot forge. A local MCP server should reject anything with an Origin it does not recognise. The spec is unambiguous: servers MUST validate it and MUST return 403 when it is present and invalid, and servers SHOULD bind only to localhost. That text has been in the transport specification through successive revisions.

A requirement that cannot fail

So why did four projects miss it in one fortnight?

Not because the capability is missing. The SDKs have it. In the TypeScript SDK the transport takes enableDnsRebindingProtection, allowedHosts and allowedOrigins; in Python it is TransportSecuritySettings(enable_dns_rebinding_protection=True). Every one of the fixes consists of passing those. The Backstage issue puts it precisely: the SDK supports the protection uniformly, "this plugin simply never passes" it.

That is the whole story. The protection is opt-in. A server that says nothing gets no validation, and a server that says nothing also passes every test its authors wrote, serves every client correctly, and looks healthy in production. The requirement is normative in prose and inert in code. There is no conformance suite that exercises it, no client that refuses to connect to a server missing it, no startup warning. A MUST that nothing checks is a comment.

You can watch the gap open inside a single codebase. In process-compose the REST routes were protected by token middleware; the MCP SSE endpoint, added later, was not. The advisory's affected-code list names two files, src/api/routes.go and src/mcp/server.go, and the security lives in the first one. Nobody removed a control. The new surface was simply built beside the old one, and the old one's protections were a property of a router it did not share.

And the pattern is not confined to small packages. Google's own toolbox has had this reported since a VRP submission in October 2025; the public issue, filed in April 2026, was closed as a p3 bug with the default still permissive. That is not an oversight. That is a considered decision that secure-by-default costs more than it is worth, made by people who had the report in hand.

The strongest objection

Here is the fair version of the counterargument: this is a beat-up. Two of the four packages are small, one of the Criticals is a hobbyist MySQL bridge, and serious agent deployments use stdio, where none of this applies. The advisory record I am reading is GitHub's, which over-represents what is packaged on npm and PyPI and says nothing about what large organisations actually run.

Most of that is true, and the last part genuinely limits what I can claim. But it concedes the point rather than answering it. The risk arrives precisely at the moment a team outgrows stdio, and that migration is presented everywhere as a transport choice: swap the constructor, keep the tools. It is not a transport choice. It is the moment your tool server stops being a subprocess and becomes a web service, with the entire browser threat model attached, and nothing in the migration path tells you so. Backstage is not a hobby project, and Google is not an inattentive maintainer. They are both on this list, which suggests the failure is structural rather than careless.

What to take from this

Three things, if you build with tools.

Know your transport, and treat a change to it as a change of threat model rather than configuration. Write down what can reach the port after the change. The honest answer for anything HTTP on a developer machine includes "every web page that developer opens."

Stop trusting 127.0.0.1 as a boundary. It excludes the network and includes the browser, and the browser is where the attacker already is. This is the intuition most worth correcting, because it feels like security and is doing almost none.

Test the refusal, not the success. curl http://localhost:8080 proving the server answers tells you nothing. Send a request with Origin: https://evil.example and confirm you get a 403. That single check is the difference between the four projects in this piece and the versions that fixed them.

There is a broader habit underneath. When a specification says MUST and an SDK makes it a flag, the flag wins, because the flag is what runs. Read defaults, not documents.

The same mcp-gitlab release fixed a second advisory in which a GraphQL query beginning with a comma was classified read-only and executed as a write. Different bug, same shape: a control that reports the wrong answer and never announces that it has. We have spent two years asking whether a model can be trusted to act. The tools we handed it to act with are turning out to be ordinary software, with ordinary flaws, guarded by controls that pass silently when they are absent. The model, in all of this, is a bystander.

What this is argued from

Reporting and primary material the piece rests on, dated at the time of writing. The interpretation is mine; the facts belong to these.

  1. Streamable HTTP transport, MCP specification 2026-07-28 Model Context Protocol · 2026-07-28
  2. GHSA-rqfv-2mw9-78g2, CVE-2026-59971, mysql-mcp-server missing Origin/Host validation GitHub Advisory Database · 2026-09-11
  3. GHSA-vmp7-252j-cwp7, CVE-2026-61568, DNS rebinding reaches local HTTP transport GitHub Advisory Database · 2026-09-15
  4. GHSA-5gm3-9crp-6g3v, CVE-2026-77339, browser DNS rebinding controls local MCP tools GitHub Advisory Database · 2026-09-18
  5. mcp-actions-backend, no DNS-rebinding protection on MCP streamable-HTTP endpoint, issue 35791 Backstage · 2026-09-19
  6. Default DNS-rebinding protection allows all domains by default, issue 3113 Google · 2026-04-22
  7. GHSA-5648-rgj9-v224, multiple safety-control bypasses in @zereight/mcp-gitlab GitHub Advisory Database · 2026-09-15

Editorials on this site are written to be argued with. If you think the reading is wrong, it probably is in some particular way, and that is the useful part.

mcpagent securitydns rebindingtool usedefaults