SA-010 Avatar validation
This commit is contained in:
@@ -2,10 +2,13 @@
|
||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.app.api.dependencies import get_current_user
|
||||
@@ -19,6 +22,9 @@ from backend.app.services.secret_store import decrypt_secret, encrypt_secret
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
MAX_AVATAR_BYTES = 2 * 1024 * 1024
|
||||
MAX_AVATAR_PIXELS = 25_000_000
|
||||
|
||||
|
||||
class UserConfigUpdate(BaseModel):
|
||||
bio: str | None = None
|
||||
@@ -320,25 +326,33 @@ async def upload_avatar(
|
||||
avatar: UploadFile = File(...),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
allowed_types = {
|
||||
'image/gif': '.gif',
|
||||
'image/jpeg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/webp': '.webp',
|
||||
}
|
||||
suffix = allowed_types.get(avatar.content_type or '')
|
||||
if suffix is None:
|
||||
if avatar.content_type not in {'image/gif', 'image/jpeg', 'image/png', 'image/webp'}:
|
||||
raise HTTPException(status_code=415, detail='Avatar must be a PNG, JPEG, GIF, or WebP image')
|
||||
|
||||
contents = await avatar.read(2 * 1024 * 1024 + 1)
|
||||
if len(contents) > 2 * 1024 * 1024:
|
||||
contents = await avatar.read(MAX_AVATAR_BYTES + 1)
|
||||
if len(contents) > MAX_AVATAR_BYTES:
|
||||
raise HTTPException(status_code=413, detail='Avatar must be 2 MB or smaller')
|
||||
|
||||
avatar_path = AVATARS_DIR / f'{user["id"]}{suffix}'
|
||||
try:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('error', Image.DecompressionBombWarning)
|
||||
with Image.open(BytesIO(contents)) as image:
|
||||
if image.width * image.height > MAX_AVATAR_PIXELS:
|
||||
raise HTTPException(status_code=413, detail='Avatar dimensions are too large')
|
||||
image.verify()
|
||||
with Image.open(BytesIO(contents)) as image:
|
||||
image.load()
|
||||
normalized = image.convert('RGBA')
|
||||
except HTTPException:
|
||||
raise
|
||||
except (Image.DecompressionBombError, Image.DecompressionBombWarning, UnidentifiedImageError, OSError, ValueError) as error:
|
||||
raise HTTPException(status_code=415, detail='Avatar content is not a valid image') from error
|
||||
|
||||
avatar_path = AVATARS_DIR / f'{user["id"]}.png'
|
||||
normalized.save(avatar_path, format='PNG', optimize=True)
|
||||
for existing_path in AVATARS_DIR.glob(f'{user["id"]}.*'):
|
||||
if existing_path != avatar_path:
|
||||
existing_path.unlink(missing_ok=True)
|
||||
avatar_path.write_bytes(contents)
|
||||
avatar_url = f'/media/{avatar_path.name}'
|
||||
|
||||
with get_connection() as conn:
|
||||
|
||||
Reference in New Issue
Block a user