fix(security): 路径穿越防护+导出行数上限+并发同步锁(3 critical/important)
- media.py: GET /api/media/{hash} 现强制 file 必须位于 Settings.media_dir
沙箱内(resolve + relative_to),杜绝 local_path 越权读取任意文件。
- export.py: /api/export/chat-html 加 EXPORT_MAX_ROWS=5000 硬上限(.limit()),
超出时仅渲染前 5000 条并插入 .export-warning 横幅,防止 OOM。
- scheduler.py: _safe_run 增加 SyncRun advisory lock(60min TTL),发现同
entity 存在 status='running' 且在 TTL 内的记录即返回 skipped SyncResult;
同时为本轮 stake 一行 wrapping SyncRun,让并发 CLI/scheduler/手动触发
能相互避让。崩溃场景由 TTL 自动恢复。
- tests: 新增 path-traversal 拒绝/export 截断警告/advisory lock skip+TTL
恢复 5 个测试;test_api_media 的 fixture 改为 MEDIA_DIR=tmp_path 以适配
新沙箱语义。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ from fastapi.responses import FileResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_session
|
||||
from app.config import get_settings
|
||||
from app.db.models import Media
|
||||
from app.scraper.media import guess_mime_type
|
||||
|
||||
@@ -41,7 +42,19 @@ async def get_media(media_hash: str, session: SessionDep) -> FileResponse:
|
||||
if row is None or not row.local_path:
|
||||
raise HTTPException(status_code=404, detail="media not found")
|
||||
|
||||
path = Path(row.local_path)
|
||||
# Path-traversal defense: resolve absolute and confirm the file lives under
|
||||
# the configured ``media_dir`` sandbox. A row whose ``local_path`` escapes
|
||||
# media_dir (an absolute path elsewhere, or a ``..``-laced value) is treated
|
||||
# as not found — never served. Defense-in-depth: even if a future bug or a
|
||||
# hostile scrape payload ever lets ``local_path`` point outside the sandbox,
|
||||
# this check keeps the file server constrained to media_dir.
|
||||
media_dir = get_settings().media_dir.resolve()
|
||||
path = Path(row.local_path).resolve()
|
||||
try:
|
||||
path.relative_to(media_dir)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="media not found")
|
||||
|
||||
if not path.is_file():
|
||||
# Row exists but the blob was removed out-of-band — treat as not-found.
|
||||
raise HTTPException(status_code=404, detail="media file missing")
|
||||
|
||||
Reference in New Issue
Block a user