Skip to content

Commit aaec41e

Browse files
bugkeepRC-CHN
andauthored
fix: prevent path traversal in file uploads (AstrBotDevs#7751)
* fix: prevent path traversal in uploads * fix: remove embedded NUL bytes from upload filenames --------- Co-authored-by: RC-CHN <1051989940@qq.com>
1 parent 9f8ce24 commit aaec41e

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

astrbot/dashboard/routes/chat.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import uuid
66
from contextlib import asynccontextmanager
77
from copy import deepcopy
8+
from pathlib import Path, PurePosixPath
89
from typing import Any, cast
910

1011
from quart import Response as QuartResponse
@@ -32,6 +33,16 @@
3233
SSE_HEARTBEAT = ": heartbeat\n\n"
3334

3435

36+
def _sanitize_upload_filename(filename: str | None) -> str:
37+
if not filename:
38+
return f"{uuid.uuid4()!s}"
39+
normalized = filename.replace("\\", "/")
40+
name = PurePosixPath(normalized).name.replace("\x00", "").strip()
41+
if name in ("", ".", ".."):
42+
return f"{uuid.uuid4()!s}"
43+
return name
44+
45+
3546
@asynccontextmanager
3647
async def track_conversation(convs: dict, conv_id: str):
3748
convs[conv_id] = True
@@ -333,7 +344,7 @@ async def post_file(self):
333344
return Response().error("Missing key: file").__dict__
334345

335346
file = post_data["file"]
336-
filename = file.filename or f"{uuid.uuid4()!s}"
347+
filename = _sanitize_upload_filename(file.filename)
337348
content_type = file.content_type or "application/octet-stream"
338349

339350
# 根据 content_type 判断文件类型并添加扩展名
@@ -346,12 +357,16 @@ async def post_file(self):
346357
else:
347358
attach_type = "file"
348359

349-
path = os.path.join(self.attachments_dir, filename)
350-
await file.save(path)
360+
attachments_dir = Path(self.attachments_dir).resolve(strict=False)
361+
file_path = (attachments_dir / filename).resolve(strict=False)
362+
if not file_path.is_relative_to(attachments_dir):
363+
return Response().error("Invalid filename").__dict__
364+
365+
await file.save(str(file_path))
351366

352367
# 创建 attachment 记录
353368
attachment = await self.db.insert_attachment(
354-
path=path,
369+
path=str(file_path),
355370
type=attach_type,
356371
mime_type=content_type,
357372
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Tests for upload filename sanitization."""
2+
3+
from astrbot.dashboard.routes.chat import _sanitize_upload_filename
4+
5+
6+
def test_sanitize_upload_filename_strips_posix_traversal():
7+
assert _sanitize_upload_filename("../../outside.txt") == "outside.txt"
8+
9+
10+
def test_sanitize_upload_filename_strips_windows_traversal():
11+
assert _sanitize_upload_filename(r"..\\..\\outside.txt") == "outside.txt"
12+
13+
14+
def test_sanitize_upload_filename_strips_fakepath():
15+
assert _sanitize_upload_filename(r"C:\\fakepath\\photo.png") == "photo.png"
16+
17+
18+
def test_sanitize_upload_filename_falls_back_for_empty_values():
19+
generated = _sanitize_upload_filename("")
20+
21+
assert generated
22+
assert generated not in {".", ".."}
23+
assert "/" not in generated
24+
assert "\\" not in generated
25+
26+
27+
def test_sanitize_upload_filename_removes_embedded_null_bytes():
28+
assert _sanitize_upload_filename("evil\x00.txt") == "evil.txt"
29+
assert _sanitize_upload_filename("\x00leading.txt") == "leading.txt"
30+
assert _sanitize_upload_filename("trailing\x00.txt\x00") == "trailing.txt"
31+
assert _sanitize_upload_filename("mid\x00dle.txt") == "middle.txt"
32+

0 commit comments

Comments
 (0)