From 05ea655f9b2260bb773207ae686443a2cb702875 Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:32:11 +0530 Subject: [PATCH] security: contain workspace path resolution --- backend/app/main.py | 9 +++++++-- backend/tests/test_main.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index d0fd629..359c165 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -264,8 +264,13 @@ def model_error_preview(raw: str) -> str: def workspace_repo(workspace_id: str) -> Path: - repo = WORKSPACES / workspace_id / "repo" - if not repo.exists(): + root = WORKSPACES.resolve() + repo = (root / workspace_id / "repo").resolve() + try: + repo.relative_to(root) + except ValueError as exc: + raise HTTPException(400, "Invalid workspace id") from exc + if not repo.is_dir(): raise HTTPException(404, "Workspace not found") return repo diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 2ffeff7..c20cdb5 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -16,6 +16,7 @@ parse_json_object, safe_branch_name, safe_repo_name, + workspace_repo, ) @@ -100,6 +101,28 @@ def test_apply_edits_is_atomic_when_later_edit_is_invalid(self) -> None: self.assertEqual(first.read_text(encoding="utf-8"), "alpha") self.assertEqual(second.read_text(encoding="utf-8"), "beta") + def test_workspace_repo_rejects_path_traversal(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) / "workspaces" + outside = Path(temp_dir) / "outside" / "repo" + root.mkdir() + outside.mkdir(parents=True) + + with patch("app.main.WORKSPACES", root): + with self.assertRaises(HTTPException) as context: + workspace_repo("../outside") + + self.assertEqual(context.exception.status_code, 400) + + def test_workspace_repo_returns_existing_repo_inside_root(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) / "workspaces" + repo = root / "safe-id" / "repo" + repo.mkdir(parents=True) + + with patch("app.main.WORKSPACES", root): + self.assertEqual(workspace_repo("safe-id"), repo.resolve()) + def test_github_headers_requires_token(self) -> None: with patch.dict(os.environ, {}, clear=True): with self.assertRaises(HTTPException) as context: