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 @@