fix: add security validations to duplication upload API
P0-1: Add MIME type whitelist validation (video files only) - Accept: mp4, mpeg, mov, avi, webm, mkv, 3gp - Reject non-video files with 415 Unsupported Media Type P0-2: Add file size limit validation - Use OSS_DIRECT_UPLOAD_MAX_MB config (default 800MB) - Check Content-Length header before reading file - Verify actual file size after reading - Return 413 Request Entity Too Large if exceeded Reference: apps/api/app/api/routes/upload.py implementation
This commit is contained in:
+4
-4
@@ -1,7 +1,7 @@
|
||||
"""Phase 2 - 查重功能:duplication_records + duplication_segments
|
||||
|
||||
Revision ID: 011
|
||||
Revises: 010
|
||||
Revision ID: 012
|
||||
Revises: 011
|
||||
Create Date: 2026-06-28
|
||||
|
||||
This migration creates two new tables:
|
||||
@@ -12,8 +12,8 @@ from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers
|
||||
revision = "011"
|
||||
down_revision = "010"
|
||||
revision = "012"
|
||||
down_revision = "011"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
@@ -30,6 +30,32 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 查重功能只接受视频文件
|
||||
ALLOWED_VIDEO_MIME_TYPES = frozenset({
|
||||
"video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo",
|
||||
"video/webm", "video/x-matroska", "video/3gpp",
|
||||
})
|
||||
|
||||
|
||||
def _validate_video_mime_type(content_type: str | None) -> str:
|
||||
"""验证视频文件的 MIME 类型,如果无效则抛出异常。"""
|
||||
if not content_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Content-Type header is required",
|
||||
)
|
||||
|
||||
# 处理带参数的类型,如 "video/mp4; charset=utf-8"
|
||||
base_type = content_type.split(";")[0].strip().lower()
|
||||
|
||||
if base_type not in ALLOWED_VIDEO_MIME_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||
detail=f"只支持视频文件。支持的类型: mp4, mpeg, mov, avi, webm, mkv, 3gp",
|
||||
)
|
||||
|
||||
return base_type
|
||||
|
||||
|
||||
def _to_record_response(record: DuplicationRecord) -> DuplicationRecordResponse:
|
||||
return DuplicationRecordResponse(
|
||||
@@ -86,6 +112,21 @@ async def upload_for_duplication(
|
||||
detail="文件名不能为空",
|
||||
)
|
||||
|
||||
# P0-1: 验证 MIME 类型(只接受视频文件)
|
||||
validated_content_type = _validate_video_mime_type(file.content_type)
|
||||
|
||||
# P0-2: 验证文件大小(参考 OSS_DIRECT_UPLOAD_MAX_MB)
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
max_size_bytes = settings.OSS_DIRECT_UPLOAD_MAX_MB * 1024 * 1024
|
||||
|
||||
# 先检查 Content-Length header(如果可用)
|
||||
if file.size is not None and file.size > max_size_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"文件超过上传限制 ({settings.OSS_DIRECT_UPLOAD_MAX_MB}MB)",
|
||||
)
|
||||
|
||||
# 读取文件内容并上传到 OSS
|
||||
file_id = uuid4().hex[:8]
|
||||
safe_filename = file.filename.replace("/", "_").replace("\\", "_")
|
||||
@@ -94,6 +135,15 @@ async def upload_for_duplication(
|
||||
try:
|
||||
content = await file.read()
|
||||
file_size = len(content)
|
||||
|
||||
# 再次检查实际文件大小
|
||||
if file_size > max_size_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"文件超过上传限制 ({settings.OSS_DIRECT_UPLOAD_MAX_MB}MB)",
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -104,7 +154,7 @@ async def upload_for_duplication(
|
||||
storage_service.upload_file(
|
||||
content,
|
||||
storage_key,
|
||||
content_type=file.content_type or "video/mp4",
|
||||
content_type=validated_content_type,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
@@ -176,7 +226,7 @@ def delete_duplication_record(
|
||||
duplication_repository: Any = Depends(get_duplication_repository),
|
||||
) -> None:
|
||||
"""删除查重记录。"""
|
||||
# 先检查记录是否存在且属于当前用户
|
||||
# 检查记录是否存在且属于当前用户
|
||||
detail_uc = GetDuplicationDetailUseCase(duplication_repository)
|
||||
record = detail_uc.execute(record_id)
|
||||
if record is None or record.user_id != authenticated_user.user.id:
|
||||
|
||||
Reference in New Issue
Block a user