From 62eb825936614a63f6c25a3219212dc69ff92d24 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 14 May 2026 12:08:48 -0500 Subject: [PATCH 1/2] Enforce size limit on avatar uploads (10 MB) upload_profile_avatar previously read the entire file into memory with await file.read() and no size check, making it trivial to exhaust server memory with an oversized upload. Switch to a chunked read loop that rejects payloads above 10 MB with HTTP 413, consistent with the sample upload (50 MB) and audio import (200 MB) endpoints that already enforce their own limits. --- backend/routes/profiles.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/routes/profiles.py b/backend/routes/profiles.py index e0f7f7fd..0a6feef8 100644 --- a/backend/routes/profiles.py +++ b/backend/routes/profiles.py @@ -225,6 +225,10 @@ async def update_profile_sample( return sample +AVATAR_MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB +AVATAR_UPLOAD_CHUNK_SIZE = 1024 * 1024 # 1 MB + + @router.post("/profiles/{profile_id}/avatar", response_model=models.VoiceProfileResponse) async def upload_profile_avatar( profile_id: str, @@ -232,9 +236,18 @@ async def upload_profile_avatar( db: Session = Depends(get_db), ): """Upload or update avatar image for a profile.""" - with tempfile.NamedTemporaryFile(delete=False, suffix=Path(file.filename).suffix) as tmp: - content = await file.read() - tmp.write(content) + suffix = Path(file.filename or "").suffix or ".png" + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: + total_size = 0 + while chunk := await file.read(AVATAR_UPLOAD_CHUNK_SIZE): + total_size += len(chunk) + if total_size > AVATAR_MAX_FILE_SIZE: + Path(tmp.name).unlink(missing_ok=True) + raise HTTPException( + status_code=413, + detail=f"Avatar file too large (max {AVATAR_MAX_FILE_SIZE // (1024 * 1024)} MB)", + ) + tmp.write(chunk) tmp_path = tmp.name try: From 031198acb1d69eecbb0d3cc64313458a5b40bf30 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 14 May 2026 15:42:28 -0500 Subject: [PATCH 2/2] fix: validate avatar upload file extension against allowed image formats Reject uploads with unsupported extensions (e.g. .exe, .php) with a 400 rather than accepting any extension. Only .png, .jpg, .jpeg, .gif, .webp, .bmp, and .svg are allowed; missing extension defaults to .png. --- backend/routes/profiles.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/routes/profiles.py b/backend/routes/profiles.py index 0a6feef8..21e6f70e 100644 --- a/backend/routes/profiles.py +++ b/backend/routes/profiles.py @@ -236,7 +236,14 @@ async def upload_profile_avatar( db: Session = Depends(get_db), ): """Upload or update avatar image for a profile.""" - suffix = Path(file.filename or "").suffix or ".png" + _ALLOWED_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"} + _raw_ext = Path(file.filename or "").suffix.lower() + if _raw_ext and _raw_ext not in _ALLOWED_IMAGE_EXTS: + raise HTTPException( + status_code=400, + detail=f"Unsupported image format '{_raw_ext}'. Allowed: {sorted(_ALLOWED_IMAGE_EXTS)}", + ) + suffix = _raw_ext if _raw_ext in _ALLOWED_IMAGE_EXTS else ".png" with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: total_size = 0 while chunk := await file.read(AVATAR_UPLOAD_CHUNK_SIZE):