Skip to content

Commit f34bbcc

Browse files
committed
Truncate untrusted peer-controlled values before logging/raising
Follow-up to 3041fd0 which truncated a client-controlled session ID. A sweep of the codebase found ~30 more locations where values controlled by the other side of the connection are interpolated unbounded into log messages, exception messages, or HTTP error responses. Server-side (client-controlled input): - sse.py: session_id query param (direct analog of 3041fd0) - transport_security.py: Host/Origin headers at WARNING - mcpserver: tool names, prompt names, resource URIs in 'Unknown X' errors and logger.exception calls - streamable_http.py: mcp-protocol-version header echoed in 400 body - auth handlers: client_id, scope, redirect_uri echoed in error responses - task handlers: task_id, pagination cursor in error messages Client-side (server-controlled input): - streamable_http.py: mcp-session-id response header, content-type header, raw InitializeResult dict - session.py: protocol_version from InitializeResult - session_group.py: MCPError str (= server's error.message verbatim) - sse.py/streamable_http.py: SSE event names Protocol-level (shared/session.py): - error.message from null-ID JSONRPCError - Response ID in 'cannot be normalized' warning - Unknown-request-ID error: was logging entire SessionMessage repr (full wire payload), now logs just the truncated ID - Dropped full JSONRPCNotification repr from validation-failure warning Also removed three debug logs in server/sse.py that stringified the full request body / parsed message on every POST via eager f-string evaluation. Truncation lengths: 32 for version strings, 64 for IDs/tokens, 128 for names/headers, 256 for URIs/messages, 512 for result dicts.
1 parent eaf971c commit f34bbcc

File tree

19 files changed

+41
-38
lines changed

19 files changed

+41
-38
lines changed

src/mcp/client/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ async def initialize(self) -> types.InitializeResult:
183183
)
184184

185185
if result.protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:
186-
raise RuntimeError(f"Unsupported protocol version from the server: {result.protocol_version}")
186+
raise RuntimeError(f"Unsupported protocol version from the server: {str(result.protocol_version)[:32]}")
187187

188188
self._server_capabilities = result.capabilities
189189

src/mcp/client/session_group.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ async def _aggregate_components(self, server_info: types.Implementation, session
352352
prompts_temp[name] = prompt
353353
component_names.prompts.add(name)
354354
except MCPError as err: # pragma: no cover
355-
logging.warning(f"Could not fetch prompts: {err}")
355+
logging.warning(f"Could not fetch prompts: {str(err)[:256]}")
356356

357357
# Query the server for its resources and aggregate to list.
358358
try:
@@ -362,7 +362,7 @@ async def _aggregate_components(self, server_info: types.Implementation, session
362362
resources_temp[name] = resource
363363
component_names.resources.add(name)
364364
except MCPError as err: # pragma: no cover
365-
logging.warning(f"Could not fetch resources: {err}")
365+
logging.warning(f"Could not fetch resources: {str(err)[:256]}")
366366

367367
# Query the server for its tools and aggregate to list.
368368
try:
@@ -373,7 +373,7 @@ async def _aggregate_components(self, server_info: types.Implementation, session
373373
tool_to_session_temp[name] = session
374374
component_names.tools.add(name)
375375
except MCPError as err: # pragma: no cover
376-
logging.warning(f"Could not fetch tools: {err}")
376+
logging.warning(f"Could not fetch tools: {str(err)[:256]}")
377377

378378
# Clean up exit stack for session if we couldn't retrieve anything
379379
# from the server.

src/mcp/client/sse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async def sse_reader(task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED):
119119
session_message = SessionMessage(message)
120120
await read_stream_writer.send(session_message)
121121
case _: # pragma: no cover
122-
logger.warning(f"Unknown SSE event: {sse.event}") # pragma: no cover
122+
logger.warning(f"Unknown SSE event: {sse.event[:64]}") # pragma: no cover
123123
except SSEError as sse_exc: # pragma: lax no cover
124124
logger.exception("Encountered SSE exception")
125125
raise sse_exc

src/mcp/client/streamable_http.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def _maybe_extract_session_id_from_response(self, response: httpx.Response) -> N
112112
new_session_id = response.headers.get(MCP_SESSION_ID)
113113
if new_session_id:
114114
self.session_id = new_session_id
115-
logger.info(f"Received session ID: {self.session_id}")
115+
logger.info(f"Received session ID: {new_session_id[:64]}")
116116

117117
def _maybe_extract_protocol_version_from_message(self, message: JSONRPCMessage) -> None:
118118
"""Extract protocol version from initialization response message."""
@@ -121,10 +121,10 @@ def _maybe_extract_protocol_version_from_message(self, message: JSONRPCMessage)
121121
# Parse the result as InitializeResult for type safety
122122
init_result = InitializeResult.model_validate(message.result, by_name=False)
123123
self.protocol_version = str(init_result.protocol_version)
124-
logger.info(f"Negotiated protocol version: {self.protocol_version}")
124+
logger.info(f"Negotiated protocol version: {self.protocol_version[:32]}")
125125
except Exception: # pragma: no cover
126126
logger.warning("Failed to parse initialization response as InitializeResult", exc_info=True)
127-
logger.warning(f"Raw result: {message.result}")
127+
logger.warning(f"Raw result: {str(message.result)[:512]}")
128128

129129
async def _handle_sse_event(
130130
self,
@@ -175,7 +175,7 @@ async def _handle_sse_event(
175175
await read_stream_writer.send(exc)
176176
return False
177177
else: # pragma: no cover
178-
logger.warning(f"Unknown SSE event: {sse.event}")
178+
logger.warning(f"Unknown SSE event: {sse.event[:64]}")
179179
return False
180180

181181
async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer: StreamWriter) -> None:
@@ -295,8 +295,10 @@ async def _handle_post_request(self, ctx: RequestContext) -> None:
295295
elif content_type.startswith("text/event-stream"):
296296
await self._handle_sse_response(response, ctx, is_initialization)
297297
else:
298-
logger.error(f"Unexpected content type: {content_type}")
299-
error_data = ErrorData(code=INVALID_REQUEST, message=f"Unexpected content type: {content_type}")
298+
logger.error(f"Unexpected content type: {content_type[:64]}")
299+
error_data = ErrorData(
300+
code=INVALID_REQUEST, message=f"Unexpected content type: {content_type[:64]}"
301+
)
300302
error_msg = SessionMessage(JSONRPCError(jsonrpc="2.0", id=message.id, error=error_data))
301303
await ctx.read_stream_writer.send(error_msg)
302304

src/mcp/server/auth/handlers/authorize.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ async def error_response(
171171
# For client_id validation errors, return direct error (no redirect)
172172
return await error_response(
173173
error="invalid_request",
174-
error_description=f"Client ID '{auth_request.client_id}' not found",
174+
error_description=f"Client ID '{auth_request.client_id[:128]}' not found",
175175
attempt_load_client=False,
176176
)
177177

src/mcp/server/auth/handlers/register.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async def handle(self, request: Request) -> Response:
6666
content=RegistrationErrorResponse(
6767
error="invalid_client_metadata",
6868
error_description="Requested scopes are not valid: "
69-
f"{', '.join(requested_scopes - valid_scopes)}",
69+
f"{', '.join(requested_scopes - valid_scopes)[:256]}",
7070
),
7171
status_code=400,
7272
)

src/mcp/server/auth/handlers/token.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ async def handle(self, request: Request):
206206
return self.response(
207207
TokenErrorResponse(
208208
error="invalid_scope",
209-
error_description=(f"cannot request scope `{scope}` not provided by refresh token"),
209+
error_description=(
210+
f"cannot request scope `{scope[:128]}` not provided by refresh token"
211+
),
210212
)
211213
)
212214

src/mcp/server/experimental/task_result_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ async def handle(
103103
while True:
104104
task = await self._store.get_task(task_id)
105105
if task is None:
106-
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {task_id}")
106+
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {task_id[:64]}")
107107

108108
await self._deliver_queued_messages(task_id, session, request_id)
109109

src/mcp/server/lowlevel/experimental.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ async def _default_get_task(
161161
) -> GetTaskResult:
162162
task = await task_support.store.get_task(params.task_id)
163163
if task is None:
164-
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {params.task_id}")
164+
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {params.task_id[:64]}")
165165
return GetTaskResult(
166166
task_id=task.task_id,
167167
status=task.status,

src/mcp/server/mcpserver/prompts/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@ async def render_prompt(
5454
"""Render a prompt by name with arguments."""
5555
prompt = self.get_prompt(name)
5656
if not prompt:
57-
raise ValueError(f"Unknown prompt: {name}")
57+
raise ValueError(f"Unknown prompt: {name[:128]}")
5858

5959
return await prompt.render(arguments, context)

0 commit comments

Comments
 (0)