fix(upload): tipo da magic bytes, non da Content-Type dichiarato (#36 punto 2)
Il Content-Type del client non era verificato: un file "x.html" dichiarato image/png veniva salvato e poi servito per estensione come text/html, eseguito dal browser. Nuova detect_file_type() (PNG/JPEG/GIF/WebP/PDF) in files.py, usata anche dal logo in settings.py; l'estensione salvata segue sempre il tipo rilevato, mai il nome file dichiarato dal client. Il logo rifiuta SVG a prescindere (puo' portare script). Upload letti a blocchi da 1MB, interrotti appena superano il limite invece di bufferizzare tutto prima di controllare la dimensione. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
"""System settings router - read/update settings, upload company logo."""
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -8,6 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.backend.config import settings
|
||||
from src.backend.database import get_db
|
||||
from src.backend.api.middleware.api_key import get_current_user, require_admin_user
|
||||
from src.backend.api.routers.files import ALLOWED_IMAGE_TYPES, detect_file_type
|
||||
from src.backend.models.orm.setting import SystemSetting
|
||||
from src.backend.models.orm.user import User
|
||||
|
||||
@@ -126,35 +125,45 @@ async def upload_company_logo(
|
||||
"""Upload company logo.
|
||||
|
||||
Saves logo to uploads/logos/ and updates company_logo_path setting.
|
||||
Only admins can upload the logo.
|
||||
Only admins can upload the logo. The type is determined from the file's
|
||||
magic bytes, not the client-declared Content-Type (#36 point 2). SVG is
|
||||
refused outright: it can itself carry script content, and there is no
|
||||
safe way to serve it inline for a logo shown on every page.
|
||||
"""
|
||||
# Validate file type (must be image)
|
||||
allowed_types = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"}
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File type {file.content_type} not allowed. Must be an image.",
|
||||
)
|
||||
|
||||
# Read file content
|
||||
content = await file.read()
|
||||
# Read in chunks, stopping as soon as the size limit is crossed instead
|
||||
# of buffering an oversized body into memory first.
|
||||
max_bytes = settings.max_upload_size_mb * 1024 * 1024
|
||||
buffer = bytearray()
|
||||
chunk_size = 1024 * 1024
|
||||
while True:
|
||||
chunk = await file.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
buffer.extend(chunk)
|
||||
if len(buffer) > max_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File size exceeds maximum {settings.max_upload_size_mb}MB",
|
||||
)
|
||||
content = bytes(buffer)
|
||||
file_size = len(content)
|
||||
|
||||
# Validate file size
|
||||
max_bytes = settings.max_upload_size_mb * 1024 * 1024
|
||||
if file_size > max_bytes:
|
||||
# Validate file type from the actual bytes (SVG never matches: refused)
|
||||
detected = detect_file_type(content)
|
||||
if detected is None or detected[0] not in ALLOWED_IMAGE_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File size {file_size} bytes exceeds maximum {settings.max_upload_size_mb}MB",
|
||||
detail="File type not allowed. Must be a recognised image (SVG is not accepted).",
|
||||
)
|
||||
_detected_mime, detected_ext = detected
|
||||
|
||||
# Create logos directory
|
||||
logos_dir = settings.upload_path / "logos"
|
||||
logos_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Use a fixed filename for the company logo
|
||||
file_extension = Path(file.filename).suffix
|
||||
logo_filename = f"company_logo{file_extension}"
|
||||
# Fixed filename for the company logo, extension forced to what was
|
||||
# actually detected (not what the client claimed or the filename said).
|
||||
logo_filename = f"company_logo{detected_ext}"
|
||||
logo_path = logos_dir / logo_filename
|
||||
|
||||
# Write file
|
||||
|
||||
Reference in New Issue
Block a user