fix: 统一 CLI/API 路径安全检查为 is_path_within_workspace

This commit is contained in:
2026-07-10 15:19:17 +08:00
parent 1eb6f44ef4
commit 298f13721c
3 changed files with 26 additions and 14 deletions
+4 -4
View File
@@ -17,7 +17,7 @@ if sys.stdout.encoding != "utf-8":
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from src.core.config import DEFAULT_CONFIG_PATH from src.core.config import DEFAULT_CONFIG_PATH
from src.core.security import is_safe_path from src.core.security import is_path_within_workspace
from src.server.deps import get_state, get_default_collection from src.server.deps import get_state, get_default_collection
app = typer.Typer( app = typer.Typer(
@@ -82,7 +82,7 @@ def ingest(
if file_paths: if file_paths:
total = 0 total = 0
for fp in file_paths: for fp in file_paths:
if not is_safe_path(fp): if not is_path_within_workspace(fp):
typer.echo(f"[SKIP] 不安全的路径: {fp}", err=True) typer.echo(f"[SKIP] 不安全的路径: {fp}", err=True)
continue continue
# 支持通配符 (shell 展开或 Python glob) # 支持通配符 (shell 展开或 Python glob)
@@ -90,7 +90,7 @@ def ingest(
if "*" in fp or "?" in fp: if "*" in fp or "?" in fp:
matches = _glob.glob(fp, recursive=True) matches = _glob.glob(fp, recursive=True)
for m in matches: for m in matches:
if not is_safe_path(m): if not is_path_within_workspace(m):
typer.echo(f"[SKIP] 不安全的路径: {m}", err=True) typer.echo(f"[SKIP] 不安全的路径: {m}", err=True)
continue continue
c = ingestor.ingest_file(m) c = ingestor.ingest_file(m)
@@ -117,7 +117,7 @@ def ingest_dir(
collection: CollectionOpt = None, collection: CollectionOpt = None,
): ):
_init_config(config) _init_config(config)
if not is_safe_path(dir_path): if not is_path_within_workspace(dir_path):
typer.echo(f"错误: 不安全的路径 — {dir_path}", err=True) typer.echo(f"错误: 不安全的路径 — {dir_path}", err=True)
raise typer.Exit(code=1) raise typer.Exit(code=1)
state = get_state() state = get_state()
+20
View File
@@ -26,3 +26,23 @@ def is_safe_path(path_str: str) -> bool:
return False return False
return True return True
def is_path_within_workspace(path_str: str) -> bool:
"""检查路径是否在当前工作目录内(防路径穿越 + 目录绑定).
同时检查:
1. 路径不含 .. 穿越组件且非绝对路径
2. resolve 后的路径位于当前工作目录内
"""
if not is_safe_path(path_str):
return False
from pathlib import Path
path = Path(path_str).resolve()
cwd = Path.cwd().resolve()
try:
common = Path(os.path.commonpath([str(path), str(cwd)]))
except ValueError:
return False
return common == cwd
+2 -10
View File
@@ -10,7 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from pydantic import BaseModel, Field, model_validator from pydantic import BaseModel, Field, model_validator
from src.core.security import is_safe_path from src.core.security import is_path_within_workspace
from src.server.auth import verify_api_key, rate_limiter from src.server.auth import verify_api_key, rate_limiter
from src.server.deps import get_state, AppState from src.server.deps import get_state, AppState
@@ -117,17 +117,9 @@ def ingest_document(
ingestor = state.get_ingestor(req.collection) ingestor = state.get_ingestor(req.collection)
try: try:
if req.file_path: if req.file_path:
if not is_safe_path(req.file_path): if not is_path_within_workspace(req.file_path):
raise HTTPException(status_code=400, detail="不允许的路径") raise HTTPException(status_code=400, detail="不允许的路径")
path = Path(req.file_path).resolve() path = Path(req.file_path).resolve()
cwd = Path.cwd().resolve()
# 用 commonpath 替代字符串 startswith 比较 (Windows 大小写安全)
try:
common = Path(os.path.commonpath([str(path), str(cwd)]))
except ValueError:
raise HTTPException(status_code=400, detail="不允许访问当前目录外的路径")
if common != cwd:
raise HTTPException(status_code=400, detail="不允许访问当前目录外的路径")
if not path.exists(): if not path.exists():
raise HTTPException(status_code=404, detail=f"文件不存在: {path.name}") raise HTTPException(status_code=404, detail=f"文件不存在: {path.name}")
count = ingestor.ingest_file(str(path)) count = ingestor.ingest_file(str(path))