diff --git a/backend/package/yuxi/services/workspace_service.py b/backend/package/yuxi/services/workspace_service.py index 970c2d18..c34aa3a0 100644 --- a/backend/package/yuxi/services/workspace_service.py +++ b/backend/package/yuxi/services/workspace_service.py @@ -11,24 +11,29 @@ from urllib.parse import quote import aiofiles from fastapi import HTTPException, UploadFile from fastapi.responses import FileResponse, StreamingResponse - from yuxi.agents.backends.sandbox.paths import _global_user_data_dir, ensure_workspace_default_files from yuxi.services.viewer_filesystem_service import _detect_preview_type from yuxi.storage.postgres.models_business import User from yuxi.utils.datetime_utils import utc_isoformat_from_timestamp from yuxi.utils.paths import WORKSPACE_DIR_NAME - EDITABLE_WORKSPACE_SUFFIXES = {".md", ".markdown", ".mdx", ".txt"} def _workspace_root(user: User) -> Path: try: - root = _global_user_data_dir(str(user.id)) / WORKSPACE_DIR_NAME + user_data_root = _global_user_data_dir(str(user.id)).resolve() + root = user_data_root / WORKSPACE_DIR_NAME except ValueError as exc: raise HTTPException(status_code=403, detail="Access denied") from exc + if root.is_symlink(): + raise HTTPException(status_code=403, detail="Access denied") root.mkdir(parents=True, exist_ok=True) resolved_root = root.resolve() + try: + resolved_root.relative_to(user_data_root) + except ValueError as exc: + raise HTTPException(status_code=403, detail="Access denied") from exc ensure_workspace_default_files(resolved_root) return resolved_root @@ -138,8 +143,17 @@ async def read_workspace_file_content(*, path: str, current_user: User) -> dict: "supported": supported, "message": message, } + try: + content = raw_content.decode("utf-8") + except UnicodeDecodeError: + return { + "content": None, + "preview_type": "unsupported", + "supported": False, + "message": "当前文件不是 UTF-8 文本,暂不支持预览", + } return { - "content": raw_content.decode("utf-8"), + "content": content, "preview_type": preview_type, "supported": supported, "message": message, diff --git a/backend/test/unit/services/test_workspace_service.py b/backend/test/unit/services/test_workspace_service.py index 798fe47d..24233ce8 100644 --- a/backend/test/unit/services/test_workspace_service.py +++ b/backend/test/unit/services/test_workspace_service.py @@ -43,6 +43,38 @@ def test_workspace_root_keeps_existing_agents_prompt_file(tmp_path: Path, monkey assert agents_file.read_text(encoding="utf-8") == "保留已有内容" +def test_workspace_root_rejects_symlink_root(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(workspace_paths.conf, "save_dir", str(tmp_path)) + user_root = tmp_path / "threads" / "shared" / "user-1" + outside_root = tmp_path / "outside" + user_root.mkdir(parents=True) + outside_root.mkdir() + (user_root / "workspace").symlink_to(outside_root, target_is_directory=True) + + with pytest.raises(HTTPException) as exc_info: + svc._workspace_root(SimpleNamespace(id="user-1")) + + assert exc_info.value.status_code == 403 + + +@pytest.mark.asyncio +async def test_read_workspace_file_content_returns_unsupported_for_non_utf8_text( + tmp_path: Path, + monkeypatch, +) -> None: + monkeypatch.setattr(workspace_paths.conf, "save_dir", str(tmp_path)) + user = SimpleNamespace(id="user-1") + root = svc._workspace_root(user) + target = root / "bad.txt" + target.write_bytes(b"\xff\xfe\x00") + + result = await svc.read_workspace_file_content(path="/bad.txt", current_user=user) + + assert result["content"] is None + assert result["preview_type"] == "unsupported" + assert result["supported"] is False + + @pytest.mark.asyncio async def test_write_workspace_file_content_updates_markdown_file(tmp_path: Path, monkeypatch) -> None: monkeypatch.setattr(workspace_paths.conf, "save_dir", str(tmp_path)) diff --git a/web/src/components/AgentFilePreview.vue b/web/src/components/AgentFilePreview.vue index 66163af2..98d25aa5 100644 --- a/web/src/components/AgentFilePreview.vue +++ b/web/src/components/AgentFilePreview.vue @@ -240,7 +240,17 @@ diff --git a/web/src/components/workspace/WorkspaceSidebar.vue b/web/src/components/workspace/WorkspaceSidebar.vue index be6a542c..582f0b60 100644 --- a/web/src/components/workspace/WorkspaceSidebar.vue +++ b/web/src/components/workspace/WorkspaceSidebar.vue @@ -17,7 +17,9 @@