Skip to content

Commit 95102c7

Browse files
authored
Stop forwarding inbound HTTP headers to unrelated remote servers (#3837)
1 parent d0bcec9 commit 95102c7

3 files changed

Lines changed: 40 additions & 10 deletions

File tree

src/fastmcp/client/transports/http.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ def __init__(
9494
)
9595
self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout)
9696

97+
self.forward_incoming_headers: bool = False
98+
9799
self._get_session_id_cb: Callable[[], str | None] | None = None
98100

99101
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
@@ -148,10 +150,14 @@ def factory(
148150
async def connect_session(
149151
self, **session_kwargs: Unpack[SessionKwargs]
150152
) -> AsyncIterator[ClientSession]:
151-
# Load headers from an active HTTP request, if available. This will only be true
152-
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
153-
# need to be forwarded to the remote server.
154-
headers = get_http_headers(include={"authorization"}) | self.headers
153+
# When used in a proxy, forward the inbound request's authorization
154+
# header to the upstream server. This is off by default so that a
155+
# plain Client used inside a server tool handler doesn't accidentally
156+
# leak the caller's credentials to an unrelated remote server.
157+
if self.forward_incoming_headers:
158+
headers = get_http_headers(include={"authorization"}) | self.headers
159+
else:
160+
headers = dict(self.headers)
155161

156162
# Configure timeout if provided, preserving MCP's 30s connect default
157163
timeout: httpx.Timeout | None = None

src/fastmcp/client/transports/sse.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def __init__(
6161

6262
self._set_auth(auth)
6363

64+
self.forward_incoming_headers: bool = False
65+
6466
self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout)
6567

6668
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
@@ -117,12 +119,16 @@ async def connect_session(
117119
) -> AsyncIterator[ClientSession]:
118120
client_kwargs: dict[str, Any] = {}
119121

120-
# load headers from an active HTTP request, if available. This will only be true
121-
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
122-
# need to be forwarded to the remote server.
123-
client_kwargs["headers"] = (
124-
get_http_headers(include={"authorization"}) | self.headers
125-
)
122+
# When used in a proxy, forward the inbound request's authorization
123+
# header to the upstream server. This is off by default so that a
124+
# plain Client used inside a server tool handler doesn't accidentally
125+
# leak the caller's credentials to an unrelated remote server.
126+
if self.forward_incoming_headers:
127+
client_kwargs["headers"] = (
128+
get_http_headers(include={"authorization"}) | self.headers
129+
)
130+
else:
131+
client_kwargs["headers"] = dict(self.headers)
126132

127133
# sse_read_timeout has a default value set, so we can't pass None without overriding it
128134
# instead we simply leave the kwarg out if it's not provided

src/fastmcp/server/providers/proxy.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,15 @@ def _create_client_factory(
752752
"""
753753
if isinstance(target, Client):
754754
client = target
755+
756+
# Plain Clients used as proxy backends also need header forwarding,
757+
# same as ProxyClient (which sets this in __init__).
758+
from fastmcp.client.transports.http import StreamableHttpTransport
759+
from fastmcp.client.transports.sse import SSETransport
760+
761+
if isinstance(client.transport, StreamableHttpTransport | SSETransport):
762+
client.transport.forward_incoming_headers = True
763+
755764
if client.is_connected() and type(client) is ProxyClient:
756765
logger.info(
757766
"Proxy detected connected ProxyClient - creating fresh sessions for each "
@@ -997,6 +1006,15 @@ def __init__(
9971006
kwargs["progress_handler"] = default_proxy_progress_handler
9981007
super().__init__(**kwargs | {"transport": transport})
9991008

1009+
# Enable forwarding of inbound HTTP headers (e.g. authorization) to
1010+
# the upstream server. This is only appropriate for proxy clients,
1011+
# where the caller's credentials should be propagated.
1012+
from fastmcp.client.transports.http import StreamableHttpTransport
1013+
from fastmcp.client.transports.sse import SSETransport
1014+
1015+
if isinstance(self.transport, StreamableHttpTransport | SSETransport):
1016+
self.transport.forward_incoming_headers = True
1017+
10001018

10011019
class StatefulProxyClient(ProxyClient[ClientTransportT]):
10021020
"""A proxy client that provides a stateful client factory for the proxy server.

0 commit comments

Comments
 (0)