fix: code review — security fixes, dedup, cleanup

- setup: validate roles against users.VALID_ROLES (Supervisor was missing)
- files: fix path traversal prefix-match edge case (is_relative_to),
  dedupe path validation into resolve_upload_path(), use logging not print
- measurements: extract shared _build_measurement_filters() helper
- client app: prevent open redirect via Referer on /set-language
- maker: guard resp.json() in parse-technical-sheet proxy
- measure/maker: extract shared file proxy into services/file_proxy.py
- measure: localize supervisor validation error messages
- annotation-editor: remove global keydown listener in destroy()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-11 12:16:29 +00:00
parent 25a788f430
commit fd571c479e
8 changed files with 141 additions and 134 deletions
+37 -49
View File
@@ -1,5 +1,5 @@
"""File upload/download/delete router for images and PDFs."""
import os
import logging
import re
from pathlib import Path
@@ -11,6 +11,8 @@ from src.backend.config import settings
from src.backend.api.middleware.api_key import get_current_user, require_maker
from src.backend.models.orm.user import User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/files", tags=["files"])
# Allowed MIME types
@@ -65,6 +67,37 @@ def sanitize_filename(filename: str) -> str:
return filename
def resolve_upload_path(file_path: str) -> Path:
"""Resolve a user-supplied relative path inside the uploads directory.
Raises:
HTTPException: 404 if the path escapes the uploads directory,
cannot be resolved, or does not point to an existing file.
"""
try:
full_path = (settings.upload_path / file_path).resolve()
if not full_path.is_relative_to(settings.upload_path.resolve()):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied",
)
except HTTPException:
raise
except (OSError, RuntimeError, ValueError):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
if not full_path.exists() or not full_path.is_file():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
return full_path
def generate_thumbnail(image_path: Path, thumbnail_path: Path, max_size: tuple[int, int] = (200, 200)):
"""Generate a thumbnail for an image."""
try:
@@ -73,7 +106,7 @@ def generate_thumbnail(image_path: Path, thumbnail_path: Path, max_size: tuple[i
img.save(thumbnail_path, quality=85)
except Exception as e:
# If thumbnail generation fails, we continue without it
print(f"Thumbnail generation failed: {e}")
logger.warning("Thumbnail generation failed for %s: %s", image_path, e)
@router.post("/upload")
@@ -164,30 +197,7 @@ async def get_file(
Requires authentication but no specific role.
"""
# Construct full path
full_path = settings.upload_path / file_path
# Security: ensure path is within uploads directory
try:
full_path = full_path.resolve()
if not str(full_path).startswith(str(settings.upload_path.resolve())):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied",
)
except Exception:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
# Check if file exists
if not full_path.exists() or not full_path.is_file():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
full_path = resolve_upload_path(file_path)
return FileResponse(full_path)
@@ -200,29 +210,7 @@ async def delete_file(
Also deletes associated thumbnail if it exists.
"""
# Construct full path
full_path = settings.upload_path / file_path
# Security: ensure path is within uploads directory
try:
full_path = full_path.resolve()
if not str(full_path).startswith(str(settings.upload_path.resolve())):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied",
)
except Exception:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
# Check if file exists
if not full_path.exists() or not full_path.is_file():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="File not found",
)
full_path = resolve_upload_path(file_path)
# Delete thumbnail if it exists
if full_path.parent.name != "thumbnails":