Files
office_translator/_bmad-output/audit-securite-2026-08-26.md
sepehr 526c87348f
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m20s
feat(translation): quality pipeline overhaul + new features (audit 2026-08-29)
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)
2026-08-29 18:38:09 +02:00

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/admin
  • ADMIN_TOKEN_SECRET, POSTGRES_PASSWORD, GRAFANA_PASSWORD, STRIPE_SECRET_KEY
  • data/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é sur GET /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.fromstring sur des parties ZIP non fiables sans resolve_entities=False ni 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 pas JWT_AVAILABLE.
  • M4. /checkout/sync : ownership conditionnel (services/payment_service.py:174-178) si session sans metadata.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. /metrics legacy 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_risk fail-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é)

  1. Rotation immédiate de tous les secrets de .env.production + purge historique git (C1).
  2. Assainir le filename de l'ingestion URL (réutiliser FileValidator._sanitize_filename) (C2).
  3. Revalider le SSRF à chaque hop de redirection (désactiver follow_redirects, suivre manuellement) (C3).
  4. Corriger la clé file_pathinput_path + âge minimum pour suppression orpheline (C4).
  5. Exiger l'authentification + ownership sur /download/{job_id} et /translations/{job_id} (H2).
  6. Limites de ratio zip (H1) ; XFF/proxy de confiance + verrouillage Redis (H4).
  7. 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.db retiré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_path reconnues + 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-multipart 0.0.9 → 0.0.20, fastapi 0.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.