From c5d49f5c08ca578a1fb31aa2d19872528002e280 Mon Sep 17 00:00:00 2001 From: Parv Patodia Date: Fri, 16 Oct 2026 10:45:01 -0700 Subject: [PATCH] fix(calibration): refuse config names that escape the calibration dir start_calibration passed config_file straight into the LeRobot device id, and LeRobot builds the calibration path as calibration_dir / f"{id}.json", so a name containing ../ pointed the calibration file outside the calibration directory. Apply the same is_valid_robot_name guard the delete endpoint and robot records already use, before any state changes. --- lelab/calibrate.py | 7 +++++++ tests/test_calibrate.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lelab/calibrate.py b/lelab/calibrate.py index ee7b6afc7..bc4e241d7 100644 --- a/lelab/calibrate.py +++ b/lelab/calibrate.py @@ -39,6 +39,7 @@ ) from lerobot.utils.utils import init_logging +from .utils.config import is_valid_robot_name from .utils.devices import safe_disconnect_device logger = logging.getLogger(__name__) @@ -157,6 +158,12 @@ def _cache_recorded_ranges(self, positions: dict[str, float]) -> None: def start_calibration(self, request: CalibrationRequest) -> dict[str, Any]: """Start calibration process""" try: + # config_file becomes the LeRobot device id, and LeRobot writes the calibration to + # calibration_dir / f"{id}.json", so reject path-traversal characters (/, \, ..) first. + # Same guard as the calibration-config delete endpoint and robot records. + if not is_valid_robot_name(request.config_file): + return {"success": False, "message": "Invalid configuration name"} + if self.status.calibration_active: return {"success": False, "message": "Calibration already active"} diff --git a/tests/test_calibrate.py b/tests/test_calibrate.py index f266a27aa..760ef9383 100644 --- a/tests/test_calibrate.py +++ b/tests/test_calibrate.py @@ -292,6 +292,20 @@ def test_calibration_manager_rejects_double_start_via_message() -> None: assert "already" in result.get("message", "").lower() +@pytest.mark.parametrize("config_file", ["", "../escape", "..", "a/b", "a\\b", " padded"]) +def test_calibration_manager_rejects_unsafe_config_name(config_file) -> None: + """config_file names the calibration JSON on disk; unsafe names never start a worker.""" + from lelab.calibrate import CalibrationManager, CalibrationRequest + + mgr = CalibrationManager() + result = mgr.start_calibration( + CalibrationRequest(device_type="robot", port="/dev/null", config_file=config_file) + ) + assert result == {"success": False, "message": "Invalid configuration name"} + assert mgr.status.calibration_active is False + assert mgr.calibration_thread is None + + def test_cleanup_device_force_releases_and_clears_when_disconnect_fails() -> None: """A failed device.disconnect() must still force-close the port and clear the device handle — otherwise the COM port stays busy and blocks the next run."""