From 81b28afdc93b3eafbcbdaade3f20f5558ed1e9c8 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Mon, 20 Jul 2026 16:21:36 +0800 Subject: [PATCH] fix: constrain custom workspace paths --- astrbot/core/workspace.py | 16 +++++----- tests/unit/test_chatui_project_service.py | 38 +++++++++++++++++------ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/astrbot/core/workspace.py b/astrbot/core/workspace.py index 00ff5c5f08..cc74742c59 100644 --- a/astrbot/core/workspace.py +++ b/astrbot/core/workspace.py @@ -95,24 +95,24 @@ def workspace_path_to_root(path: str) -> Path: Args: path: Stored workspace path. Relative values are rooted under AstrBot - workspaces. Absolute values are allowed and resolved as provided. + workspaces. Absolute values must also stay within AstrBot workspaces. Returns: Absolute resolved path. Raises: - ValueError: If a relative path escapes or targets the AstrBot workspaces - root. + ValueError: If the path escapes or targets the AstrBot workspaces root. """ workspaces_root = Path(get_astrbot_workspaces_path()).resolve(strict=False) candidate = Path(path).expanduser() - if candidate.is_absolute(): - return candidate.resolve(strict=False) - - resolved = (workspaces_root / candidate).resolve(strict=False) + resolved = ( + candidate.resolve(strict=False) + if candidate.is_absolute() + else (workspaces_root / candidate).resolve(strict=False) + ) if resolved == workspaces_root or not resolved.is_relative_to(workspaces_root): raise ValueError( - "Relative workspace path must stay within a subdirectory of AstrBot workspaces" + "Custom workspace path must stay within a subdirectory of AstrBot workspaces" ) return resolved diff --git a/tests/unit/test_chatui_project_service.py b/tests/unit/test_chatui_project_service.py index 3aaacb5e31..26dd890971 100644 --- a/tests/unit/test_chatui_project_service.py +++ b/tests/unit/test_chatui_project_service.py @@ -60,9 +60,7 @@ def test_custom_workspace_rejects_file_path(tmp_path, monkeypatch): ) -def test_custom_workspace_relative_path_uses_astrbot_workspaces( - tmp_path, monkeypatch -): +def test_custom_workspace_relative_path_uses_astrbot_workspaces(tmp_path, monkeypatch): """Relative custom workspace paths should resolve under AstrBot workspaces.""" relative_workspace = tmp_path / "relative-workspace" relative_workspace.mkdir() @@ -118,14 +116,14 @@ def test_custom_workspace_rejects_workspaces_root(tmp_path, monkeypatch): ) -def test_custom_workspace_accepts_absolute_path_outside_workspaces( +def test_custom_workspace_accepts_absolute_path_inside_workspaces( tmp_path, monkeypatch ): - """Absolute custom workspace paths may point outside AstrBot workspaces.""" - outside_workspace = tmp_path / "outside" + """Absolute custom workspace paths may point inside AstrBot workspaces.""" workspaces_root = tmp_path / "workspaces" - outside_workspace.mkdir() workspaces_root.mkdir() + custom_workspace = workspaces_root / "custom" + custom_workspace.mkdir() monkeypatch.setattr( "astrbot.core.workspace.get_astrbot_workspaces_path", lambda: str(workspaces_root), @@ -134,9 +132,31 @@ def test_custom_workspace_accepts_absolute_path_outside_workspaces( workspace_type, workspace_path = ChatUIProjectService._normalize_workspace_config( { "workspace_type": "custom", - "workspace_path": str(outside_workspace), + "workspace_path": str(custom_workspace), } ) assert workspace_type == "custom" - assert workspace_path == str(outside_workspace) + assert workspace_path == str(custom_workspace) + + +def test_custom_workspace_rejects_absolute_path_outside_workspaces( + tmp_path, monkeypatch +): + """Absolute custom workspace paths must not escape AstrBot workspaces.""" + outside_workspace = tmp_path / "outside" + workspaces_root = tmp_path / "workspaces" + outside_workspace.mkdir() + workspaces_root.mkdir() + monkeypatch.setattr( + "astrbot.core.workspace.get_astrbot_workspaces_path", + lambda: str(workspaces_root), + ) + + with pytest.raises(ChatUIProjectServiceError, match="must stay within"): + ChatUIProjectService._normalize_workspace_config( + { + "workspace_type": "custom", + "workspace_path": str(outside_workspace), + } + )