feat: durcit l'exécution SQL IA, borne les uploads et ajoute des tests

- sql_executor: remplace le filtre regex fragile par une autorisation SQLite
  (set_authorizer) en complément de mode=ro ; rejette les instructions multiples
- uploads: lecture bornée des PDF (helper read_upload_limited, limite 20 Mo,
  HTTP 413) branchée sur /extract et /save-with-pdf
- tests: suite pytest (54 tests) couvrant amounts, dates, storage, sql_executor,
  uploads et DatabaseService.save_document ; pytest ajouté en dépendance dev

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 10:11:07 +02:00
parent c15b65f0d9
commit 635a094591
13 changed files with 646 additions and 22 deletions

View File

@@ -0,0 +1,48 @@
"""Lecture bornée des fichiers uploadés (protection mémoire / DoS)."""
from fastapi import UploadFile
# Taille maximale acceptée pour un PDF uploadé (20 Mo)
MAX_UPLOAD_SIZE = 20 * 1024 * 1024
_CHUNK_SIZE = 1024 * 1024 # 1 Mo
class UploadTooLargeError(Exception):
"""Levée quand un fichier uploadé dépasse la taille maximale autorisée."""
def __init__(self, max_size: int):
self.max_size = max_size
super().__init__(
f"Fichier trop volumineux (maximum {max_size // (1024 * 1024)} Mo)"
)
async def read_upload_limited(
file: UploadFile, max_size: int = MAX_UPLOAD_SIZE
) -> bytes:
"""Lit un fichier uploadé par morceaux en plafonnant la taille totale.
Évite de charger en mémoire un fichier arbitrairement gros.
Args:
file: Fichier uploadé (FastAPI UploadFile).
max_size: Taille maximale en octets.
Returns:
Contenu binaire du fichier.
Raises:
UploadTooLargeError: si le fichier dépasse ``max_size``.
"""
chunks: list[bytes] = []
total = 0
while True:
chunk = await file.read(_CHUNK_SIZE)
if not chunk:
break
total += len(chunk)
if total > max_size:
raise UploadTooLargeError(max_size)
chunks.append(chunk)
return b"".join(chunks)