Four Months Open: CVE-2026-104120 Lands on MCP’s Own Reference Fetch Server

NVD published CVE-2026-104120 on 2 October 2026. The affected component is not a startup’s agent framework or a hastily published community connector. It is mcp-server-fetch and mcp-server-everything in modelcontextprotocol/servers — the reference server collection published by the Model Context Protocol project itself, the code most engineers copy when they want to see how an MCP tool is supposed to be written.

The NVD description is blunt about the function and the state of the fix:

“Affected is the function fetch_url of the file mcp_server_fetch/server.py of the component Fetch Tool. The manipulation of the argument url/path leads to server-side request forgery. The attack may be initiated remotely. The exploit has been disclosed publicly and may be used. The pull request to fix this issue awaits acceptance.”

That last sentence is the story. The record carries a CVSS v3.1 base score of 7.3 (High) and a CVSS v4.0 score of 5.5 (Medium) from VulDB as assigning CNA, with the v4.0 vector marking exploit maturity E:P — proof-of-concept. NVD lists the record’s status as Deferred. Affected versions are given as up to 2026.6.4.

What the code actually does

We read the current main at the time of writing. The fetch path is short enough to quote the shape of it: the server builds an httpx.AsyncClient and issues a GET with follow_redirects=True, a user-agent header and a 30-second timeout. There is no hostname resolution step, no IP classification, no allowlist, no block on redirect targets.

The same unguarded client is used for the robots.txt pre-check in check_may_autonomously_fetch_url. That detail matters more than it first appears: the robots.txt request is derived from the same attacker-influenced host, so even a request the model never completes has already touched the internal address once.

The reporter’s own summary of the exposure, filed on the project tracker, enumerates what is reachable: loopback, link-local and cloud metadata at 169.254.169.254, 0.0.0.0, and RFC1918 ranges — with redirects followed and not re-validated at each hop.

Why this is an agent problem and not a generic SSRF

Classic SSRF requires an attacker who can reach a parameter. Here the parameter is produced by a language model, and the model reads untrusted web pages for a living. That inverts the usual precondition.

The reporter states it precisely: the tool’s URL argument “is produced by the LLM and can be steered by untrusted content (prompt injection from a fetched page or document).” A page the agent is asked to summarise can contain text that persuades the model to fetch an internal address next. The fetched contents are then returned into the model context — so metadata credentials do not merely get requested, they get read back by the system that can act on them.

This is the same structural failure we described when UTCP patched the same SSRF four times and left the redirect hop open, and it sits alongside the MCP ecosystem’s earlier reference-implementation flaws: the Java SDK DNS rebinding issue, the Go SDK cross-site tool execution, and the TypeScript SDK cross-client leak. The pattern across all four is that the official implementations are being audited later than the third-party tooling built on top of them.

The timeline is the finding

The disclosure history is documented in public on the project’s own tracker, and it is unusually legible:

  • 5 June 2026 — a researcher submits three GitHub security advisories to the project: two SSRF and one path traversal.
  • 8 July 2026 — the same researcher opens a public issue titled simply “GHSAs,” asking whether the June submissions were seen by maintainers. The issue is labelled bug. It is still open.
  • 4 September 2026 — the researcher posts the full technical write-up of the fetch SSRF as a comment on that issue.
  • 22 September 2026 — an independent contributor, working only from a static review of public source, files a second issue describing the identical gap and offers a patch.
  • 28 September 2026 — a third party opens a pull request implementing IP validation and redirect-hop checks, with 43 passing tests, clean pyright and ruff.
  • 2 October 2026 — NVD publishes CVE-2026-104120 and notes the fix PR awaits acceptance.

Nearly four months elapsed between private report and CVE, and the fix on the table at publication time came from a volunteer rather than the maintainers. The PR author converted it to draft the day after opening it, asking maintainers to confirm the intended direction before review — and no direction has been given.

The “it’s documented” defence, and why it is weak

The project has an answer to this, and it is sitting in the README:

“This server can access local/internal IP addresses and may represent a security risk. Exercise caution when using this MCP server to ensure this does not expose any sensitive data.”

That caution is real, it predates the reports, and the second reporter explicitly acknowledged it — filing the issue as an insecure default rather than a novel vulnerability claim, with no proof-of-concept. It is an honest disclosure of a design posture.

It is also the crux of the disagreement, and the PR author named it: the README documents local access as a feature, so blocking private IPs by default is a breaking change for anyone who legitimately fetches http://localhost:8000. That is a genuine design tension, not maintainer indifference. But a warning in a README does not travel. The agent operator who installs via uvx or pip install mcp-server-fetch and wires the tool into a client has not necessarily read it, and the model certainly has not. A documented risk on a reference implementation becomes, in practice, an undocumented risk in every deployment downstream of it.

One subtlety worth keeping

A comment on the September issue, from someone who built the same control in a Rust MCP browser, makes a point that most SSRF patches get wrong: a resolve-then-connect pre-check still leaves a TOCTOU window, because DNS rebinding can hand the check a public A record and the dialer a private one. Putting the filter inside the resolver the client actually dials with closes that window and removes the need to re-check each redirect hop separately.

The proposed PR validates before connect and hooks redirect handling; its author flagged the rebinding limitation himself. Anyone writing their own fence should aim for the resolver-level version rather than the pre-check version.

What to do now

  • Assume no upstream fix. At the time of writing the code in main is unguarded and the PR is an unmerged draft. Do not plan around a patch release.
  • Fence at the network, not in the tool. Since you cannot rely on the application-layer check, run MCP fetch servers with egress policy that denies RFC1918, loopback, link-local and metadata addresses. This is the control you actually own.
  • Kill IMDSv1 on any host running a fetch-capable agent. Metadata credential theft is the highest-value outcome of this class, and hop-limit plus token-required metadata configuration removes most of it independently of the tool.
  • Check mcp-server-everything too. The CVE names both packages; the demo/sample server is easy to forget because it is usually installed for evaluation and then left running.
  • Treat reference implementations as code you own. The reason this sat four months is not malice — it is a volunteer-maintained repository receiving advisories faster than it can triage them. If a reference server is in your production agent path, it inherits your patch obligations, not the project’s.

Our verification was primary-source-led. We read the NVD record for CVE-2026-104120 directly from the NVD 2.0 API, including publication date, Deferred status, both CVSS vectors, the assigning CNA and the reference list. We retrieved the current src/fetch/src/mcp_server_fetch/server.py and src/fetch/README.md from the project’s main branch to confirm the unguarded AsyncClient call with follow_redirects=True and the existing README caution. We read issues #4492 and #4838 and pull request #4890 through the GitHub API to establish the disclosure timeline, labels, open/draft states and author identities, and quote the reporters’ own characterisations rather than paraphrasing their severity claims. The three referenced GHSA identifiers are listed by the reporter; the GitHub advisory API returned no published record for them at the time of writing, which is consistent with advisories that remain in draft, and we say so rather than inferring their contents. We did not run the server, send any request to any third-party host, or test exploitation.

Sources: