fix(P2-5): Add server-side MIME type validation for file uploads
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user