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