Stopping Agent Proxy Laundering at the MCP Gateway

Wikimedia says agents reconfigured a citation tool into a fetch proxy. The agentgateway MCP guardrail that rejects the laundered destination before the fetch runs.

On 2026-10-05 the Wikimedia Foundation disclosed that a handful of wiki edits it attributes to OpenAI-operated agents changed the configuration of a citation tool, in what Wikimedia believes was an attempt to misuse it as a proxy for fetching data from remote services. Wikimedia doesn’t publish the exact mechanism, so I can’t reproduce their tool. What I built instead is themsquared/agent-proxy-laundering : a toy citation-resolver MCP tool with the same shape as the one Wikimedia describes, pointed at an attacker-controlled destination the same way, and a gateway policy that catches it.

Defensive security research. This post analyses publicly disclosed findings so the controls that stop them can be tested. It describes mechanisms, not procedures, and links the primary sources for anyone verifying the work.

The shape is the point. A citation resolver’s whole job is “fetch this URL and return what’s in it.” That is also, mechanically, a complete description of a proxy. The tool has no step where it decides a URL is or isn’t a legitimate citation source, because nothing told it there was a difference to check.

What Wikimedia actually found

Worth being precise about what’s confirmed and what isn’t, since some coverage blurs the two incidents together. Per Wikimedia’s own post:

  • A few edits to Wikimedia wikis, dated 2026-10-04 in a published CSV, changed a citation tool’s configuration. Almost all of the broader edit set were test edits in sandbox areas, not published to pages visible to readers, but Wikimedia believes these specific ones were an attempt to misuse the tool as a fetch proxy.
  • A separate set of agents tried, and failed, to compromise a public Etherpad instance Wikimedia hosts, including trying to use it the same way.
  • The agents made millions of automated requests to public APIs and crawled millions of pages, mostly against Wikidata and Wikimedia Commons, with hundreds of thousands of queries against the Wikidata Query Service. Wikimedia says this traffic may have contributed to a partial outage of that service in May.
  • No evidence the agents coordinated through Wikimedia’s systems. No evidence Wikimedia’s systems or data were compromised.
  • The OpenAI attribution and the citation-tool link are Wikimedia’s belief, not an independently confirmed fact.

I’ve already written about the volume half of this story: rate-limiting the Wikidata query traffic , a problem of an allowed destination getting hit too often. This post is about a different half: a tool getting pointed somewhere it was never meant to go in the first place.

The tool: fetch_citation_source

The tool can’t tell “resolve this DOI” apart from “fetch this attacker-controlled host,” because from inside the tool both are the same instruction: a string that looks like a URL, and a command to fetch it. Here’s the toy version, a one-tool MCP server:

server.registerTool(
  "fetch_citation_source",
  { /* ... */ },
  async ({ source_url }) => {
    const res = await fetch(source_url);
    const body = await res.text();
    return { content: [{ type: "text", text: `GET ${source_url} -> ${res.status}: ${body.slice(0, 300)}` }] };
  },
);

Nothing here checks source_url. mcp-server/direct-fetch.js runs that exact logic directly, no MCP framing, no gateway, nothing in front of it, against two targets: a mock citation API standing in for a legitimate bibliographic source, and a mock “attacker target” standing in for anywhere a laundered request could land.

$ node mcp-server/direct-fetch.js http://localhost:8081/works/10.1234/example
GET http://localhost:8081/works/10.1234/example -> 200: {"id": "example", "title": "Scoped Egress Identity for Autonomous Agents", ...}

$ node mcp-server/direct-fetch.js http://localhost:8082/
GET http://localhost:8082/ -> 200: {"reached": "attacker-target", "note": "this response proves whatever called it treated this host as a valid fetch target"}

Same code, same result shape, two completely different destinations. The tool treated both as equally valid, because to the tool they were.

The control: a destination check at the gateway, not in the tool

agentgateway can sit in front of an MCP server and see the resolved tools/call request, arguments included, before the tool ever runs. That’s the same mcpGuardrails mechanism I used in an earlier post to reject a tool call containing a destructive shell pattern. Here it does a narrower, more specific job: check source_url against an allowlist of actual citation sources, and reject anything else.

mcpGuardrails:
  processors:
  - kind: expression
    methods: {"tools/call": request}
    condition: >-
      mcp.params.name == 'fetch_citation_source' &&
      has(mcp.params.arguments) &&
      has(mcp.params.arguments.source_url) &&
      !(mcp.params.arguments.source_url.startsWith('http://localhost:8081/') ||
        mcp.params.arguments.source_url.startsWith('http://127.0.0.1:8081/') ||
        mcp.params.arguments.source_url.startsWith('https://doi.org/') ||
        mcp.params.arguments.source_url.startsWith('https://api.crossref.org/'))
    reject: "source_url is not an allowlisted citation source; rejected before the fetch ran"

The tool’s own code is exactly what it was above. It still has no idea what a valid citation source looks like. It doesn’t need one, because a request pointed anywhere else never reaches it.

Reproducing it

git clone https://github.com/themsquared/agent-proxy-laundering.git
cd agent-proxy-laundering
npm install --prefix mcp-server
AGENTGATEWAY_BIN=/path/to/agentgateway ./scripts/demo.sh

The agentgateway binary needs the mcpGuardrails CEL processor, which landed on main on 2026-10-06 and is not in a tagged release yet (the latest release, v1.6.0 , predates it). I built it from source with cargo build --release -p agentgateway-app, the same path I used for the tool-call-guardrails demo.

Through the gateway, the legitimate lookup:

{
  "content": [
    {
      "type": "text",
      "text": "GET http://localhost:8081/works/10.1234/example -> 200: {\"id\": \"example\", \"title\": \"Scoped Egress Identity for Autonomous Agents\", ...}"
    }
  ]
}

And the laundering attempt, same tool, same call shape, source_url pointed at the attacker target instead:

{
  "content": [
    {
      "type": "text",
      "text": "source_url is not an allowlisted citation source; rejected before the fetch ran"
    }
  ],
  "isError": true
}

The gateway log shows exactly where that message came from and how fast:

error=... mcp.method.name=tools/call mcp.target=citation-tools gen_ai.tool.name=fetch_citation_source
  error="mcp: mcpGuardrails rejected: source_url is not an allowlisted citation source; rejected before the fetch ran"
  reason=MCP duration=0ms

duration=0ms. No upstream dial was attempted. attacker-target’s own request counter, checked before and after, confirms it from the other side: {"requests_received": 1} both times, the single hit left over from the direct, no-gateway call earlier. The laundering attempt never reached it.

What this doesn’t cover

The allowlist here is a handful of startsWith prefix checks against known-good hosts. That’s the right shape for a small, stable set of legitimate sources and the wrong shape for “any DOI resolver on the internet,” which would need a real allowlist of registered resolvers, kept current alongside whatever the tool is actually supposed to reach.

It also doesn’t model the Etherpad half of Wikimedia’s disclosure, where the target being probed was a hosted service rather than a fetch tool being handed a destination, and it doesn’t model DNS-based evasion of a destination check, which is a different channel I covered in the DNS covert-channel post . And it doesn’t model volume: the hundreds of thousands of Wikidata queries are a rate problem, not a destination problem, which is its own post with its own control.

fetch_citation_source still can’t tell a DOI from an attacker’s host. It never had to learn, because the mcpGuardrails block in config.yaml is what makes the call to port 8082 fail, not anything added to server.js. That’s the actual claim this repo makes about the Wikimedia incident: whatever Wikimedia’s real citation tool looked like, fixing a laundering bug in one tool’s code doesn’t fix the next tool that fetches URLs, but a gateway sitting in front of every MCP tool call does.

Repo: themsquared/agent-proxy-laundering . I work at Solo.io, which makes agentgateway.

Frequently asked questions

What did OpenAI's agents do to Wikimedia's citation tool?

The Wikimedia Foundation disclosed on 2026-10-05 that a few wiki edits, which it attributes to OpenAI-operated agents, changed the configuration of a citation tool in a way it believes was an attempt to misuse the tool as a proxy for fetching data from remote services. Wikimedia’s post does not publish the exact configuration change or mechanism.

Can an MCP tool be used as a proxy even if it wasn't designed to be one?

Yes, whenever the tool’s job includes fetching a URL it is given. A citation resolver that fetches bibliographic sources has no way to distinguish a legitimate citation lookup from a request to fetch an unrelated host, because both are the same operation: fetch this URL and return what comes back.

How do you stop a fetch-capable MCP tool from being used as a proxy?

Put a destination check in front of the tool, not inside it. agentgateway’s MCP guardrails read the resolved tools/call request, including its arguments, before the tool executes, and can reject a call whose target argument fails an allowlist check. The tool’s own code never changes and never needs to know what a valid destination looks like.