fix: reject HEIC as MP4, close UploadFile, narrow exception handling

- Add ftyp brand allowlist to reject HEIC/HEIF files misclassified as MP4
- Close UploadFile after read to release resources during processing
- Add exception chaining (from None) on HTTPException raises
- Narrow except to ValueError/OSError (let programming errors propagate)
- Add exc_info=True to thumbnail failure log for debuggability
- Type detect_file_type return as tuple[MediaType, str]
This commit is contained in:
Fringg
2026-03-23 12:42:21 +03:00
parent ce554cb2a8
commit 7ff73e8492
2 changed files with 15 additions and 9 deletions
+6 -5
View File
@@ -66,6 +66,7 @@ async def upload_media(
# Read slightly over the max allowed size so we can detect oversized files.
absolute_max_bytes = settings.MEDIA_MAX_VIDEO_SIZE_MB * _BYTES_PER_MB + 1
data = await file.read(absolute_max_bytes)
await file.close()
if not data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -85,7 +86,7 @@ async def upload_media(
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail='Unsupported file type. Allowed: JPEG, PNG, WebP, MP4, WebM',
)
) from None
# Enforce per-type size limits
max_size_mb = (
@@ -110,12 +111,12 @@ async def upload_media(
)
else:
saved = await save_video(data, upload_path)
except Exception:
logger.exception('Failed to save uploaded media', media_type=media_type)
except (ValueError, OSError) as exc:
logger.warning('Failed to save uploaded media', media_type=media_type, error=str(exc))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail='Failed to process uploaded file',
)
) from None
logger.info(
'Media uploaded',
+9 -4
View File
@@ -66,7 +66,10 @@ def ensure_upload_dirs(upload_path: Path) -> None:
(upload_path / subdir).mkdir(parents=True, exist_ok=True)
def detect_file_type(data: bytes) -> tuple[str, str]:
MediaType = Literal['image', 'video']
def detect_file_type(data: bytes) -> tuple[MediaType, str]:
"""Detect media type and extension from magic bytes.
Returns:
@@ -88,8 +91,10 @@ def detect_file_type(data: bytes) -> tuple[str, str]:
if data[: len(signature)] == signature:
return 'image', ext
# Check MP4: bytes 4-7 must be 'ftyp'
if len(data) >= 8 and data[4:8] == b'ftyp':
# Check MP4: bytes 4-7 must be 'ftyp', bytes 8-12 must be a known video brand.
# Rejects HEIC/HEIF images (ftypheic, ftypmif1, etc.) which share the ftyp box format.
_MP4_VIDEO_BRANDS = {b'isom', b'mp41', b'mp42', b'M4V ', b'avc1', b'iso5', b'iso6', b'mmp4', b'dash', b'mp71'}
if data[4:8] == b'ftyp' and data[8:12] in _MP4_VIDEO_BRANDS:
return 'video', '.mp4'
# Check standard video signatures
@@ -170,7 +175,7 @@ def _process_and_save_image(
except Exception:
tmp_thumb.unlink(missing_ok=True)
# Non-fatal: log and continue without thumbnail
logger.warning('Failed to generate thumbnail', filename=filename)
logger.warning('Failed to generate thumbnail', filename=filename, exc_info=True)
thumbnail_filename = None
relative_path = f'{_IMAGES_DIR}/{filename}'