From 401aa807ee3a3e1a363a7c2d52a6da8cfdd642ce Mon Sep 17 00:00:00 2001 From: Drishna Trivedi Date: Wed, 20 May 2026 14:23:56 +0530 Subject: [PATCH] fix(proxy/cwe-209): strip Python traceback from HTTP 503 error responses The /cache/ping endpoint included a full Python traceback in its 503 error response body (inside the ProxyException message), leaking internal file paths, line numbers, and call stacks to any caller. Two MCP route handlers in proxy_server.py similarly interpolated str(e) into "Internal server error" detail strings. Fix: log the traceback server-side via verbose_proxy_logger.exception() and omit it from the ProxyException payload / HTTPException detail returned to clients. Tests updated to assert no "traceback" keyword or frame paths appear in the 503 body, with a new dedicated regression test. CWE-209: Generation of Error Message Containing Sensitive Information. Co-Authored-By: Claude Sonnet 4.6 --- litellm/proxy/caching_routes.py | 4 +--- litellm/proxy/proxy_server.py | 12 +++++----- .../test_litellm/proxy/test_caching_routes.py | 24 ++++++++++++++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/caching_routes.py b/litellm/proxy/caching_routes.py index 20c951d350d..7a2c965c38b 100644 --- a/litellm/proxy/caching_routes.py +++ b/litellm/proxy/caching_routes.py @@ -98,13 +98,11 @@ async def cache_ping(): litellm_cache_params=safe_dumps(litellm_cache_params), ) except Exception as e: - import traceback - + verbose_proxy_logger.exception("Cache health check failed: %s", str(e)) error_message = { "message": f"Service Unhealthy ({str(e)})", "litellm_cache_params": safe_dumps(litellm_cache_params), "health_check_cache_params": safe_dumps(cleaned_cache_params), - "traceback": traceback.format_exc(), } raise ProxyException( message=safe_dumps(error_message), diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 879914e5ac6..2accd7afcd3 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -15696,10 +15696,10 @@ async def toolset_mcp_route(toolset_name: str, request: Request): except HTTPException as e: raise e except Exception as e: - verbose_proxy_logger.error( - f"Error handling toolset MCP route for {toolset_name}: {str(e)}" + verbose_proxy_logger.exception( + "Error handling toolset MCP route for %s: %s", toolset_name, str(e) ) - raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") async def _mcp_forward_as_path(path_segment: str, request: Request): @@ -15877,7 +15877,7 @@ async def dynamic_mcp_route(mcp_server_name: str, request: Request): except HTTPException as e: raise e except Exception as e: - verbose_proxy_logger.error( - f"Error handling dynamic MCP route for {mcp_server_name}: {str(e)}" + verbose_proxy_logger.exception( + "Error handling dynamic MCP route for %s: %s", mcp_server_name, str(e) ) - raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") diff --git a/tests/test_litellm/proxy/test_caching_routes.py b/tests/test_litellm/proxy/test_caching_routes.py index 3e842d118dd..2ad750c7be4 100644 --- a/tests/test_litellm/proxy/test_caching_routes.py +++ b/tests/test_litellm/proxy/test_caching_routes.py @@ -123,12 +123,34 @@ def test_cache_ping_failure(mock_redis_failure): assert "message" in error_details assert "litellm_cache_params" in error_details assert "health_check_cache_params" in error_details - assert "traceback" in error_details # Verify specific error message assert "invalid username-password pair" in error_details["message"] +def test_cache_ping_failure_does_not_expose_traceback(mock_redis_failure): + """CWE-209: Stack trace must not appear in the HTTP 503 response body.""" + response = client.get("/cache/ping", headers={"Authorization": "Bearer sk-1234"}) + assert response.status_code == 503 + + data = response.json() + error = data.get("error", {}) + raw_body = json.dumps(data) + + # The word "traceback" (case-insensitive) must not appear anywhere in the response + assert ( + "traceback" not in raw_body.lower() + ), "CWE-209: Python traceback exposed in HTTP 503 response body" + # Internal frame paths should not leak either + assert ( + 'File "' not in raw_body + ), "CWE-209: Python stack frame paths exposed in HTTP 503 response body" + + # The error message field should still describe the failure + error_details = json.loads(error["message"]) + assert "invalid username-password pair" in error_details["message"] + + def test_cache_ping_no_cache_initialized(): """Test cache ping when no cache is initialized""" # Set cache to None