Files
roboco/tests/unit/services/test_workspace_path_segments.py
7be725cc13 fix(security): disposition all 104 code-scanning + dependabot alerts (#375)
Fix the 4 real CodeQL path-injection alerts (open_conventions_pr trusted the
API-settable project.workspace_path with no containment) plus defense-in-depth
segment validation at the get_workspace_path chokepoint. Bump next 16.1.1->16.1.7
and transitive lockfile deps to clear 24 Dependabot alerts. Close the intake
subagent-ban gap: the Claude intake driver still carried the Task tool and the
prompter prompt told it to fan out research subagents, contradicting the
fleet-wide ban. The remaining 47 CodeQL + 29 Dependabot alerts are dismissed on
GitHub with per-alert justifications (guard patterns CodeQL can't model across
call hops; next 16.2.x blocked by the verified tab-hostage router regression).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-10 00:15:04 +02:00

57 lines
1.9 KiB
Python

"""``get_workspace_path`` rejects traversal-capable path components.
Slugs/teams are regex- or enum-validated at creation, but every workspace path
is built at this one chokepoint — pin the by-construction guard so a raw
``../`` / absolute / NUL segment can never place a workspace outside the root,
regardless of what upstream validation a future caller forgets.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock
import pytest
from roboco.models.base import Team
from roboco.services.workspace import WorkspaceError, WorkspaceService
if TYPE_CHECKING:
from pathlib import Path
def _service(root: Path) -> WorkspaceService:
svc = WorkspaceService(MagicMock())
svc.session = AsyncMock()
svc.root = root
return svc
@pytest.mark.parametrize(
"bad",
["", ".", "..", "../escape", "a/b", "a\\b", "bad\x00slug", "/etc"],
)
def test_rejects_unsafe_project_slug(tmp_path: Path, bad: str) -> None:
svc = _service(tmp_path)
with pytest.raises(WorkspaceError, match="unsafe project slug"):
svc.get_workspace_path(bad, Team.BACKEND, "be-dev-1")
@pytest.mark.parametrize("bad", ["..", "back/end", ""])
def test_rejects_unsafe_team_string(tmp_path: Path, bad: str) -> None:
svc = _service(tmp_path)
with pytest.raises(WorkspaceError, match="unsafe team"):
svc.get_workspace_path("guard-core", bad, "be-dev-1")
@pytest.mark.parametrize("bad", ["..", "../../be-dev-1", "be\x00dev"])
def test_rejects_unsafe_agent_slug(tmp_path: Path, bad: str) -> None:
svc = _service(tmp_path)
with pytest.raises(WorkspaceError, match="unsafe agent slug"):
svc.get_workspace_path("guard-core", Team.BACKEND, bad)
def test_valid_segments_unchanged(tmp_path: Path) -> None:
svc = _service(tmp_path)
path = svc.get_workspace_path("guard-core", Team.BACKEND, "be-dev-1")
assert path == tmp_path / "guard-core" / "backend" / "be-dev-1"