fix(P2-5): Add server-side MIME type validation for file uploads

This commit is contained in:
2026-06-26 18:21:46 +08:00
parent f2b98aa259
commit e925d9f43b
+62 -2
View File
@@ -1,3 +1,4 @@
import os
from typing import Any
from uuid import uuid4
@@ -26,6 +27,59 @@ from packages.ports.workspace_member_repository import WorkspaceMemberRepository
router = APIRouter()
# Allowed MIME types for uploads
ALLOWED_VIDEO_TYPES = {"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm", "video/x-matroska"}
ALLOWED_AUDIO_TYPES = {"audio/mpeg", "audio/wav", "audio/ogg", "audio/flac", "audio/aac"}
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"}
ALLOWED_CONTENT_TYPES = ALLOWED_VIDEO_TYPES | ALLOWED_AUDIO_TYPES | ALLOWED_IMAGE_TYPES
# Extension to MIME type mapping
EXTENSION_TO_MIME = {
".mp4": "video/mp4",
".mov": "video/quicktime",
".avi": "video/x-msvideo",
".webm": "video/webm",
".mkv": "video/x-matroska",
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".flac": "audio/flac",
".aac": "audio/aac",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".svg": "image/svg+xml",
}
def _get_mime_type_from_filename(filename: str) -> str:
"""Get MIME type from file extension."""
_, ext = os.path.splitext(filename.lower())
return EXTENSION_TO_MIME.get(ext, "application/octet-stream")
def _validate_content_type(content_type: str, filename: str) -> str:
"""Validate and normalize content type.
If content_type is not provided or invalid, derive from filename.
"""
if content_type and content_type in ALLOWED_CONTENT_TYPES:
return content_type
# Try to get from filename
mime_from_file = _get_mime_type_from_filename(filename)
if mime_from_file in ALLOWED_CONTENT_TYPES:
return mime_from_file
# Content type not allowed
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File type '{content_type or mime_from_file}' is not allowed. "
f"Allowed types: video, audio, and image files.",
)
def _require_project_and_library(
workspace_id: str,
@@ -81,6 +135,9 @@ async def prepare_direct_upload(
detail=f"File exceeds upload limit ({settings.OSS_DIRECT_UPLOAD_MAX_MB}MB)",
)
# P2-5: Validate content type on server side
validated_content_type = _validate_content_type(request.content_type, request.filename)
require_workspace_member(request.workspace_id, authenticated_user, workspace_member_repository)
_require_project_and_library(
request.workspace_id,
@@ -96,7 +153,7 @@ async def prepare_direct_upload(
try:
payload = storage_service.create_direct_upload_post(
storage_key=storage_key,
content_type=request.content_type or "application/octet-stream",
content_type=validated_content_type,
max_size_bytes=max_size_bytes,
expires_seconds=settings.OSS_DIRECT_UPLOAD_EXPIRE_SECONDS,
)
@@ -165,13 +222,16 @@ async def upload_asset(
require_workspace_member(workspace_id, authenticated_user, workspace_member_repository)
_require_project_and_library(workspace_id, project_id, library_id, project_repository, asset_library_repository)
# P2-5: Validate and normalize content type on server side
validated_content_type = _validate_content_type(file.content_type, file.filename)
file_id = uuid4().hex[:8]
storage_key = f"uploads/{file_id}/{file.filename}"
file_url = storage_service.upload_file(
file.file,
storage_key,
content_type=file.content_type or "application/octet-stream",
content_type=validated_content_type,
)
job = _submit_ingest_job(