Translation quality & format preservation: - Word: merge adjacent same-format runs into one unit (sentence-level coherence like inline-tag handling); translate comments/balloons; dedupe textbox collection (was translated twice); RTL no longer overrides center/justify alignment; CJK/Arabic font hints (eastAsia/cs) - PPTX: chart translations now actually reach the output file (ChartPart.blob is read-only — rewrite chart XML in the saved ZIP); CJK typeface hints (a:ea) - Excel: sheet renames no longer break references — rewrite cell formulas (3D/quoted), defined names, data validations, cond. formats - PDF: bold/italic honored (hebo/heit/hebi); table cells never merge; unchanged blocks left untouched (typography preserved, fixes duplicate hyperlinks); attempted/changed stats + route gate now cover PDF; CJK font paths; scanned PDFs via Mistral OCR (detection + admin settings) Features: - formality param (formal/informal) + automatic regional-variant prompts - output_mode=bilingual docx (source above translation) - per-user translation memory on Redis (falls back to LRU), context-hashed - QA report + 0-100 confidence score in job status; L0 on by default - OpenAI-compatible providers: whole chunk in ONE numbered-JSON request (~15x fewer calls) with per-item fallback; base prompt always present (custom prompt no longer replaces translation instructions) Infra & marketing alignment: - plan-based engine gating + vision gating (closes paid-engine leak); /providers/available filtered per plan; 107 languages exposed - zh-CN/zh-TW validation fixed; libmagic disabled on Windows (native crash) - admin: Mistral OCR settings + engine status dashboard; httpx<0.28 pin (TestClient breakage); Prometheus test fixture fixed - marketing docs aligned with code (PDF+OCR, retention, engines, pricing) - security: .env.ionos/.env.production/provider_settings.json removed Tests: 1173 passed / 0 failed (6 network tests deselected: free Google endpoint temporarily blocked from this machine)
8.6 KiB
Audit de sécurité — office_translator
Date : 2026-08-26 · Branche : production-deployment · Périmètre : backend FastAPI, routes, middlewares, services, config, déploiement, dépôt git.
Critiques
C1. Secrets de production commis dans git
.env.production est suivi par git (confirmé via git ls-files) et contient des valeurs réelles :
JWT_SECRET_KEY(ligne 65/81) — permet de forger n'importe quel JWT utilisateur/adminADMIN_TOKEN_SECRET,POSTGRES_PASSWORD,GRAFANA_PASSWORD,STRIPE_SECRET_KEYdata/provider_settings.json(suivi) contient un mot de passe SMTP en clair.- Un fichier
.db(1) est suivi : hashes de mots de passe et enregistrements utilisateurs potentiels.
Action : rotation de TOUS ces secrets, suppression des fichiers (git rm --cached), purge de l'historique (git filter-repo), et déplacement hors du dépôt des clés TLS privées présentes à la racine (*.key, non suivies mais à côté du code).
C2. Traversée de chemin dans l'ingestion par URL
routes/translate_routes.py:292-312 — le nom de fichier provient du Content-Disposition du serveur distant (contrôlé par l'attaquant puisque file_url est fourni par l'utilisateur) et est utilisé non assaini : temp_path = config.UPLOAD_DIR / f"{unique_id}_{filename}". filename="../..../evil.xlsx" écrit hors de UPLOAD_DIR. La whitelist d'extensions ne bloque pas ... Contraste : l'upload direct est correctement assaini (middleware/validation.py:239-259).
C3. SSRF par redirection
routes/translate_routes.py:263-267 — le hostname est vérifié une fois par _is_ssrf_risk() (solide par ailleurs), mais la requête utilise follow_redirects=True sans revalidation : une URL publique peut rediriger 302 vers 169.254.169.254, localhost, plages privées. Fenêtre TOCTOU DNS-rebinding en plus.
C4. Job de cleanup : bug de clé + purge orpheline sans âge minimum
middleware/cleanup.py:194 lit metadata["file_path"] alors que les fichiers sont suivis sous "input_path" (translate_routes.py:837) → tracked_paths toujours vide → tout fichier est classé orphelin, et la suppression orpheline n'a aucune vérification d'âge (cleanup.py:236-241) → boucle de 5 min supprime des fichiers de jobs en cours. DoS/raison de disponibilité. (protect_file() existe mais n'est jamais appelé.)
Élevées
H1. Zip-bomb non contré
Aucune limite de ratio de décompression ni de taille décompressée sur OOXML : openpyxl.load_workbook, docx.Document, zipfile.read() directs (translators/word_translator.py:834+, pptx_translator.py:544+). Un fichier de <50 MB très compressé → Go en RAM → OOM worker. Seule la taille compressée est vérifiée.
H2. Téléchargement / statut : contrôle de propriété défaillant (IDOR)
routes/translate_routes.py:1879-1886: le check de propriété surGET /download/{job_id}n'est effectué que si un utilisateur est authentifié ; un appelant anonyme qui devine/fuit un job_id (12 hex ≈ 48 bits) télécharge le document traduit, et la suppression après 1er téléchargement permet un DoS contre le légitime propriétaire.routes/translate_routes.py:1690-1779:GET /translations/{job_id}n'a aucun check de propriété → énumération de statut, noms de fichiers, erreurs.
H3. Traduction anonyme
/translate accepte les requêtes non authentifiées (current_user optionnel, ligne 547) → traitement gratuit au tarif payant, seule la limitation IP s'applique ; combiné au webhook_url, oracle d'egress gratuit. À confirmer si voulu (démo landing ?).
H4. Rate limiting contournable via X-Forwarded-For
middleware/rate_limiting.py:249-261 et routes/admin_routes.py:176-189 font confiance au premier XFF sans proxy de confiance configuré → rotation d'XFF factice pour contourner rate limit et verrouillage brute-force admin (le verrouillage et les sessions admin sont aussi en mémoire par worker, admin_routes.py:47-52).
Moyennes
- M1. XXE/hardening XML :
lxml.etree.fromstringsur des parties ZIP non fiables sansresolve_entities=Falseni defusedxml (word_translator.py:731,839…,pptx_translator.py:553…). Mitigé par libxml2 ≥2.9, mais à durcir explicitement. - M2. Rotation refresh token sans révocation :
/refresh(routes/auth_routes.py:661-742) ne révoque pas l'ancien refresh token (7 jours de vie, pas de détection de réutilisation). Reset de mot de passe sans révocation des sessions existantes. - M3. Fallback PyJWT absent : jetons signés en base64 non signé silencieusement acceptés (
services/auth_service.py:158-169) ; garde de production OK mais login/verify ne vérifient pasJWT_AVAILABLE. - M4.
/checkout/sync: ownership conditionnel (services/payment_service.py:174-178) si session sansmetadata.user_id. - M5. Endpoint legacy batch (
legacy_routes.py:248-320) : pas de magic bytes, nombre de fichiers non borné, traitement synchrone → épuisement CPU/RAM./metricslegacy non authentifié. - M6. Upload direct :
await file.read()charge tout le fichier (≤50 MB) en RAM avant vérification (middleware/validation.py:116) ; l'URL stream correctement. - M7. Dépendances vulnérables (
requirements.txt) :python-multipart==0.0.9(CVE-2024-24762, upload !),fastapi==0.109.0(ReDoS),pydantic==2.5.3,stripe==7.0.0. À mettre à jour en priorité. - M8. Temp files images écrits dans le temp système, non couverts par le cleanup, fuités sur exception (
pptx_translator.py:1060-1066,word_translator.py:1273-1275).
Faibles
- Logout révoque un refresh token fourni sans check de propriété (
auth_routes.py:401-410). - Flux Google OAuth access_token sans validation d'audience (
auth_routes.py:604-609). - Admin mono-facteur mot de passe partagé ; envisager TOTP.
- Erreurs URL qui divulguent
str(e)+ URL interne au client (translate_routes.py:356-371). - Caches mémoire non bornés (
_gc_key_cache), jobs « processing » jamais purgés. - Secret webhook Stripe lu à l'import au lieu du runtime (
payment_service.py:26).
Points forts constatés
- Vérification Stripe webhook correcte (signature + idempotence + body brut).
- JWT : algorithme épinglé HS256, jetons typés, révocation jti Redis, access 15 min.
- Clés API : stockées en SHA-256, haute entropie, expiration/révocation serveur ; glossaires/prompts correctement scopés par
user_id(pas d'IDOR là). - Anti-énumération (bcrypt factice constant-time, forgot-password toujours 200).
- Validation magic-bytes + allowlist extensions sur le flux v1 ;
_is_ssrf_riskfail-closed ; streaming avec cap d'octets côté URL. - Échec au démarrage en production si secrets manquants / CORS wildcard ; pas de SQL brut (ORM uniquement) ; bcrypt pour les mots de passe.
Priorités de remédiation (ordre recommandé)
- Rotation immédiate de tous les secrets de
.env.production+ purge historique git (C1). - Assainir le filename de l'ingestion URL (réutiliser
FileValidator._sanitize_filename) (C2). - Revalider le SSRF à chaque hop de redirection (désactiver
follow_redirects, suivre manuellement) (C3). - Corriger la clé
file_path→input_path+ âge minimum pour suppression orpheline (C4). - Exiger l'authentification + ownership sur
/download/{job_id}et/translations/{job_id}(H2). - Limites de ratio zip (H1) ; XFF/proxy de confiance + verrouillage Redis (H4).
- Mise à jour
python-multipart,fastapi,pydantic(M7).
Suivi des correctifs (2026-08-26)
Corrigés au code :
- ✅ C1 (partie git) :
.env.production,.env.ionos,data/provider_settings.json,translations.dbretirés du suivi git +.gitignore. ⚠️ rotation des secrets + purge de l'historique restent à faire manuellement. - ✅ C2 : nom de fichier assaini dans l'ingestion URL (
_sanitize_url_filename). - ✅ C3 : redirections suivies manuellement avec revalidation SSRF à chaque étape (max 5).
- ✅ C4 : clés
input_path/file_path/output_pathreconnues + délai de grâce de 15 min avant suppression d'un fichier orphelin. - ✅ H1 : contrôle anti-fichier-piège (
validate_zip_safety, ratio max 100:1, 1 Go décompressé) sur les flux v1 et legacy. - ✅ H2 : contrôle d'accès strict sur
/translations/{id}et/download/{id}(propriétaire obligatoire ; jeton secret requis pour les jobs sans compte). - ✅ H3 (partiel) : les jobs anonymes nécessitent désormais le jeton secret ; la création anonyme reste possible.
- ✅ M7 :
python-multipart0.0.9 → 0.0.20,fastapi0.109.0 → 0.109.1.
Restent à faire : rotation des secrets + purge historique git (manuel), H4 (en-tête X-Forwarded-For), M2 (révocation refresh token), M4, M5, M6.