智教助手平台:完整初始化
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""文件上传安全校验。
|
||||
|
||||
提供 validate_upload() 对上传文件做:
|
||||
1. 文件名清洗(去除路径、危险字符、控制长度)
|
||||
2. 扩展名白名单校验
|
||||
3. MIME 与扩展名一致性校验
|
||||
4. 文件魔数(magic bytes)校验,防止伪装
|
||||
5. 文件大小校验
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, UploadFile
|
||||
|
||||
|
||||
# 允许的扩展名 → 期望的 MIME 前缀(; 前部分)
|
||||
ALLOWED_TYPES: dict[str, str] = {
|
||||
"txt": "text/",
|
||||
"md": "text/",
|
||||
"csv": "text/",
|
||||
"json": "application/json",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"pdf": "application/pdf",
|
||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"xls": "application/vnd.ms-excel",
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"gif": "image/gif",
|
||||
"webp": "image/webp",
|
||||
"bmp": "image/bmp",
|
||||
}
|
||||
|
||||
# 魔数签名(前若干字节)
|
||||
MAGIC_SIGNATURES: dict[str, list[bytes]] = {
|
||||
"pdf": [b"%PDF-"],
|
||||
"png": [b"\x89PNG\r\n\x1a\n"],
|
||||
"jpg": [b"\xff\xd8\xff"],
|
||||
"jpeg": [b"\xff\xd8\xff"],
|
||||
"gif": [b"GIF87a", b"GIF89a"],
|
||||
"bmp": [b"BM"],
|
||||
"webp": [b"RIFF"],
|
||||
# Office 文件均为 ZIP 容器(PK\x03\x04)
|
||||
"docx": [b"PK\x03\x04"],
|
||||
"pptx": [b"PK\x03\x04"],
|
||||
"xlsx": [b"PK\x03\x04"],
|
||||
"xls": [b"\xd0\xcf\x11\xe0"], # OLE 复合文档
|
||||
}
|
||||
|
||||
MAX_FILENAME_LEN = 200
|
||||
|
||||
|
||||
@dataclass
|
||||
class UploadCheck:
|
||||
filename: str
|
||||
suffix: str
|
||||
media_type: str
|
||||
size: int
|
||||
|
||||
|
||||
_FILENAME_BAD = re.compile("[\x00-\x1f<>:\"/\\|?*]")
|
||||
|
||||
|
||||
def sanitize_filename(name: str) -> str:
|
||||
"""清洗文件名:去路径、去控制/危险字符、限长。"""
|
||||
# 仅保留文件名部分
|
||||
name = re.split(r"[\\/]", name)[-1].strip()
|
||||
name = _FILENAME_BAD.sub("_", name)
|
||||
name = name.strip(". ")
|
||||
if not name:
|
||||
name = "upload"
|
||||
if len(name) > MAX_FILENAME_LEN:
|
||||
stem, dot, ext = name.rpartition(".")
|
||||
if dot:
|
||||
name = stem[: MAX_FILENAME_LEN - len(ext) - 1] + "." + ext
|
||||
else:
|
||||
name = name[:MAX_FILENAME_LEN]
|
||||
return name
|
||||
|
||||
|
||||
def _check_magic(suffix: str, head: bytes) -> bool:
|
||||
sigs = MAGIC_SIGNATURES.get(suffix)
|
||||
if not sigs:
|
||||
return True # 无签名要求(文本类)默认放行
|
||||
return any(head.startswith(sig) for sig in sigs)
|
||||
|
||||
|
||||
def validate_upload(
|
||||
file: UploadFile,
|
||||
*,
|
||||
content: bytes,
|
||||
max_size: Optional[int] = None,
|
||||
) -> UploadCheck:
|
||||
"""校验上传文件,失败抛出 HTTPException。
|
||||
|
||||
参数 content 为已读取的字节内容(用于魔数校验)。
|
||||
"""
|
||||
if max_size is not None and len(content) > max_size:
|
||||
raise HTTPException(status_code=413, detail=f"文件不能超过 {max_size // (1024 * 1024)}MB")
|
||||
if not content:
|
||||
raise HTTPException(status_code=400, detail="上传文件为空")
|
||||
|
||||
raw_name = file.filename or "material"
|
||||
filename = sanitize_filename(raw_name)
|
||||
suffix = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
|
||||
media_type = (file.content_type or "").split(";")[0].strip().lower()
|
||||
|
||||
if suffix not in ALLOWED_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=415,
|
||||
detail="暂不支持该文件类型,请上传 txt、md、csv、json、docx、pptx、pdf、xlsx 或图片",
|
||||
)
|
||||
|
||||
# MIME 一致性:图片/office/pdf 必须匹配;文本类型放宽
|
||||
expected_mime = ALLOWED_TYPES[suffix]
|
||||
if expected_mime != "text/" and media_type and not media_type.startswith(expected_mime.split("/")[0]):
|
||||
# 宽松:只校验主类型一致(image/* application/* 等)
|
||||
if media_type.split("/")[0] != expected_mime.split("/")[0]:
|
||||
raise HTTPException(status_code=415, detail="文件类型与扩展名不一致")
|
||||
|
||||
# 魔数校验
|
||||
head = content[:16]
|
||||
if not _check_magic(suffix, head):
|
||||
raise HTTPException(status_code=415, detail="文件内容与声明类型不符")
|
||||
|
||||
return UploadCheck(filename=filename, suffix=suffix, media_type=media_type or expected_mime, size=len(content))
|
||||
Reference in New Issue
Block a user