""" Shared upload validation — sniffs the real file content (libmagic) rather than trusting the client-supplied Content-Type header, and enforces a size cap. See port log A5: the archive only checked `file.content_type`, which is attacker-controlled and proves nothing about what's actually in the body. """ import magic from fastapi import HTTPException, UploadFile DEFAULT_MAX_BYTES = 20 * 1024 * 1024 # 20 MB — matches nginx client_max_body_size async def read_and_validate_upload( file: UploadFile, allowed_mimes: set[str], max_bytes: int = DEFAULT_MAX_BYTES, ) -> bytes: """ Read an UploadFile fully, verify its sniffed MIME type is in `allowed_mimes`, and enforce `max_bytes`. Returns the file bytes for the caller to save/process. Raises HTTPException(400) on any failure. """ content = await file.read() if not content: raise HTTPException(status_code=400, detail="Empty file") if len(content) > max_bytes: raise HTTPException( status_code=400, detail=f"File too large — max {max_bytes // (1024 * 1024)}MB", ) sniffed = magic.from_buffer(content, mime=True) if sniffed not in allowed_mimes: raise HTTPException( status_code=400, detail=f"File content doesn't match an allowed type (detected: {sniffed})", ) return content