mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-24 16:39:52 +08:00
fix: prevent path traversal in file uploads (#7751)
* fix: prevent path traversal in uploads * fix: remove embedded NUL bytes from upload filenames --------- Co-authored-by: RC-CHN <1051989940@qq.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import re
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from copy import deepcopy
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, cast
|
||||
|
||||
from quart import Response as QuartResponse
|
||||
@@ -32,6 +33,16 @@ from .route import Response, Route, RouteContext
|
||||
SSE_HEARTBEAT = ": heartbeat\n\n"
|
||||
|
||||
|
||||
def _sanitize_upload_filename(filename: str | None) -> str:
|
||||
if not filename:
|
||||
return f"{uuid.uuid4()!s}"
|
||||
normalized = filename.replace("\\", "/")
|
||||
name = PurePosixPath(normalized).name.replace("\x00", "").strip()
|
||||
if name in ("", ".", ".."):
|
||||
return f"{uuid.uuid4()!s}"
|
||||
return name
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def track_conversation(convs: dict, conv_id: str):
|
||||
convs[conv_id] = True
|
||||
@@ -333,7 +344,7 @@ class ChatRoute(Route):
|
||||
return Response().error("Missing key: file").__dict__
|
||||
|
||||
file = post_data["file"]
|
||||
filename = file.filename or f"{uuid.uuid4()!s}"
|
||||
filename = _sanitize_upload_filename(file.filename)
|
||||
content_type = file.content_type or "application/octet-stream"
|
||||
|
||||
# 根据 content_type 判断文件类型并添加扩展名
|
||||
@@ -346,12 +357,16 @@ class ChatRoute(Route):
|
||||
else:
|
||||
attach_type = "file"
|
||||
|
||||
path = os.path.join(self.attachments_dir, filename)
|
||||
await file.save(path)
|
||||
attachments_dir = Path(self.attachments_dir).resolve(strict=False)
|
||||
file_path = (attachments_dir / filename).resolve(strict=False)
|
||||
if not file_path.is_relative_to(attachments_dir):
|
||||
return Response().error("Invalid filename").__dict__
|
||||
|
||||
await file.save(str(file_path))
|
||||
|
||||
# 创建 attachment 记录
|
||||
attachment = await self.db.insert_attachment(
|
||||
path=path,
|
||||
path=str(file_path),
|
||||
type=attach_type,
|
||||
mime_type=content_type,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Tests for upload filename sanitization."""
|
||||
|
||||
from astrbot.dashboard.routes.chat import _sanitize_upload_filename
|
||||
|
||||
|
||||
def test_sanitize_upload_filename_strips_posix_traversal():
|
||||
assert _sanitize_upload_filename("../../outside.txt") == "outside.txt"
|
||||
|
||||
|
||||
def test_sanitize_upload_filename_strips_windows_traversal():
|
||||
assert _sanitize_upload_filename(r"..\\..\\outside.txt") == "outside.txt"
|
||||
|
||||
|
||||
def test_sanitize_upload_filename_strips_fakepath():
|
||||
assert _sanitize_upload_filename(r"C:\\fakepath\\photo.png") == "photo.png"
|
||||
|
||||
|
||||
def test_sanitize_upload_filename_falls_back_for_empty_values():
|
||||
generated = _sanitize_upload_filename("")
|
||||
|
||||
assert generated
|
||||
assert generated not in {".", ".."}
|
||||
assert "/" not in generated
|
||||
assert "\\" not in generated
|
||||
|
||||
|
||||
def test_sanitize_upload_filename_removes_embedded_null_bytes():
|
||||
assert _sanitize_upload_filename("evil\x00.txt") == "evil.txt"
|
||||
assert _sanitize_upload_filename("\x00leading.txt") == "leading.txt"
|
||||
assert _sanitize_upload_filename("trailing\x00.txt\x00") == "trailing.txt"
|
||||
assert _sanitize_upload_filename("mid\x00dle.txt") == "middle.txt"
|
||||
|
||||
Reference in New Issue
Block a user