From 95102c7d7c7f1847d64c23a342a335294a8deb94 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:14:38 -0400 Subject: [PATCH] Stop forwarding inbound HTTP headers to unrelated remote servers (#3837) --- src/fastmcp/client/transports/http.py | 14 ++++++++++---- src/fastmcp/client/transports/sse.py | 18 ++++++++++++------ src/fastmcp/server/providers/proxy.py | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/fastmcp/client/transports/http.py b/src/fastmcp/client/transports/http.py index 5ff52a091..bfed0fa00 100644 --- a/src/fastmcp/client/transports/http.py +++ b/src/fastmcp/client/transports/http.py @@ -94,6 +94,8 @@ class StreamableHttpTransport(ClientTransport): ) self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout) + self.forward_incoming_headers: bool = False + self._get_session_id_cb: Callable[[], str | None] | None = None def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None): @@ -148,10 +150,14 @@ class StreamableHttpTransport(ClientTransport): async def connect_session( self, **session_kwargs: Unpack[SessionKwargs] ) -> AsyncIterator[ClientSession]: - # Load headers from an active HTTP request, if available. This will only be true - # if the client is used in a FastMCP Proxy, in which case the MCP client headers - # need to be forwarded to the remote server. - headers = get_http_headers(include={"authorization"}) | self.headers + # When used in a proxy, forward the inbound request's authorization + # header to the upstream server. This is off by default so that a + # plain Client used inside a server tool handler doesn't accidentally + # leak the caller's credentials to an unrelated remote server. + if self.forward_incoming_headers: + headers = get_http_headers(include={"authorization"}) | self.headers + else: + headers = dict(self.headers) # Configure timeout if provided, preserving MCP's 30s connect default timeout: httpx.Timeout | None = None diff --git a/src/fastmcp/client/transports/sse.py b/src/fastmcp/client/transports/sse.py index fa900eb7b..43bdb7c96 100644 --- a/src/fastmcp/client/transports/sse.py +++ b/src/fastmcp/client/transports/sse.py @@ -61,6 +61,8 @@ class SSETransport(ClientTransport): self._set_auth(auth) + self.forward_incoming_headers: bool = False + self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout) def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None): @@ -117,12 +119,16 @@ class SSETransport(ClientTransport): ) -> AsyncIterator[ClientSession]: client_kwargs: dict[str, Any] = {} - # load headers from an active HTTP request, if available. This will only be true - # if the client is used in a FastMCP Proxy, in which case the MCP client headers - # need to be forwarded to the remote server. - client_kwargs["headers"] = ( - get_http_headers(include={"authorization"}) | self.headers - ) + # When used in a proxy, forward the inbound request's authorization + # header to the upstream server. This is off by default so that a + # plain Client used inside a server tool handler doesn't accidentally + # leak the caller's credentials to an unrelated remote server. + if self.forward_incoming_headers: + client_kwargs["headers"] = ( + get_http_headers(include={"authorization"}) | self.headers + ) + else: + client_kwargs["headers"] = dict(self.headers) # sse_read_timeout has a default value set, so we can't pass None without overriding it # instead we simply leave the kwarg out if it's not provided diff --git a/src/fastmcp/server/providers/proxy.py b/src/fastmcp/server/providers/proxy.py index ed8c61b94..b59024c70 100644 --- a/src/fastmcp/server/providers/proxy.py +++ b/src/fastmcp/server/providers/proxy.py @@ -752,6 +752,15 @@ def _create_client_factory( """ if isinstance(target, Client): client = target + + # Plain Clients used as proxy backends also need header forwarding, + # same as ProxyClient (which sets this in __init__). + from fastmcp.client.transports.http import StreamableHttpTransport + from fastmcp.client.transports.sse import SSETransport + + if isinstance(client.transport, StreamableHttpTransport | SSETransport): + client.transport.forward_incoming_headers = True + if client.is_connected() and type(client) is ProxyClient: logger.info( "Proxy detected connected ProxyClient - creating fresh sessions for each " @@ -997,6 +1006,15 @@ class ProxyClient(Client[ClientTransportT]): kwargs["progress_handler"] = default_proxy_progress_handler super().__init__(**kwargs | {"transport": transport}) + # Enable forwarding of inbound HTTP headers (e.g. authorization) to + # the upstream server. This is only appropriate for proxy clients, + # where the caller's credentials should be propagated. + from fastmcp.client.transports.http import StreamableHttpTransport + from fastmcp.client.transports.sse import SSETransport + + if isinstance(self.transport, StreamableHttpTransport | SSETransport): + self.transport.forward_incoming_headers = True + class StatefulProxyClient(ProxyClient[ClientTransportT]): """A proxy client that provides a stateful client factory for the proxy server.