Files
zero/docs
Vasanth T 8e9c3ec4ce fix(mcp): scrub the injected AI/ML API key from server responses (#1197)
* fix(mcp): scrub the injected AI/ML API key from server responses

Zero injects the user's AI/ML API key into requests to mcp.aimlapi.com
(#1193) and keeps it out of config, but only an HTTP error status left
the server's text out. A JSON-RPC error message, the method of an
unexpected message, a tool result (MCP reports a failed tool call as a
result with isError, not as an error) and a legacy SSE endpoint that
fails to parse all reached the caller as the server wrote them, so a
server that echoed the key put it in errors, in tool output sent to the
model, and in anything that prints them.

A response to a request that carried the injected key now has the key
replaced with [REDACTED] in its error text, its method and every string
of its result before anything reads it, on streamable HTTP and on the
legacy SSE transport. The SSE client scrubs both the key its stream
request carried and the one on the POST, since the key is resolved per
request. A user Authorization header, OAuth and project-configured
servers are unchanged, since nothing was injected.

The scrub uses a new redaction.RedactSecretValues: the split-aware
known-value pass RedactString applies to ExtraSecretValues, without the
shape patterns, which could otherwise rewrite tool output or base64
payloads that never held the key. A result without the key keeps its
original bytes.

* fix(mcp): scrub every key a call sent, and transport errors too

Review found two gaps. The key is resolved again for each redirect hop,
but the scrub used only the final hop's key, so a same-origin server
answering the last hop could echo an earlier hop's key. And a legacy SSE
POST that failed at the transport quoted the endpoint URL the server
chose, which can hold the stream's key, with no scrub at all.

A collector in the request context now records every key
applyAIMLAPIRequestHeaders injects during a call. net/http gives each
redirect hop the first request's context, so a later hop's key is
recorded too. Both transports scrub the response and every error the
call returns with those keys, the stream request's own errors included,
and the SSE client adds the keys its stream request sent. A scrubbed
error unwraps only to the context sentinel the original matched, so a
cancellation still reads as one without the chain that held the key.

Reported by @jatmn.
2026-11-09 07:49:09 -08:00
..