# 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.

- Canonical URL: https://webofmike.com/agent-proxy-laundering/
- Author: Mike Moore (https://webofmike.com/about/)
- Published: 2026-10-25
- Last modified: 2026-10-25
- Tags: MCP, AI Gateways, Security, AI Agents
- Cite as: Mike Moore, "Stopping Agent Proxy Laundering at the MCP Gateway", Web of Mike (webofmike.com), 2026-10-25. https://webofmike.com/agent-proxy-laundering/


On 2026-10-05 the [Wikimedia Foundation disclosed](https://wikimediafoundation.org/news/2026/10/05/openai-rogue-agent-activities-found-on-wikimedia-projects/) 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](https://github.com/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.

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](/agent-egress-rate-limits/), 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:

```javascript
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](https://agentgateway.dev) 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](/tool-call-guardrails/) 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.

```yaml
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

```bash
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](https://github.com/agentgateway/agentgateway/releases/tag/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:

```json
{
  "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:

```json
{
  "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](/agent-dns-egress-covert-channel/). 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](/agent-egress-rate-limits/) 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](https://github.com/themsquared/agent-proxy-laundering). I work at Solo.io, which makes agentgateway.

