Skip to main content

Bugs & failles actives connues

Contrairement à Dette technique connue (différable, pas urgent), tout ce qui est listé ici est un dysfonctionnement ou un risque de sécurité déjà actif en l'état du code — pas une amélioration future. À traiter en priorité, pas "quand l'occasion se présente".

Dernière vérification contre le code

Vérifié le 2026-08-13, complété le 2026-08-24, puis recorrigé le 2026-08-24 après un second git pull du backend qui a fait apparaître la route "mot de passe oublié" (section "❌ Non corrigés").

❌ Non corrigés​

"Mot de passe oublié" backoffice — la route backend existe désormais, mais deux URL codées en dur cassent encore le flux​

Mise à jour le 2026-08-24 (second pull) : contrairement à ce que cette page indiquait plus tôt dans la journée, le backend main a maintenant POST /api/auth/forgot-password/POST /api/auth/reset-password (cdde188, auth.controller.js) — implémentation par ailleurs propre : ne révèle jamais si l'email existe, token crypto.randomBytes(32) unique en base (users.reset_token, expirant 15 min), regex de mot de passe fort à la réinitialisation.

Reste cassé pour deux raisons indépendantes, une de chaque côté :

  1. Backend — services/mailer.js::sendResetPasswordEmail construit le lien avec `http://localhost:5173/admin/reset-password?token=${token}` codé en dur, alors que process.env.FRONTEND_URL existe déjà dans .env.example et est déjà utilisé pour ce cas exact ailleurs (utils/mailer.js, corrigé le 2026-08-13 — voir Dette technique connue). Un email de réinitialisation envoyé depuis un serveur en production pointera donc vers localhost, injoignable pour le destinataire.
  2. Frontend (non re-vérifié à cette date, seul le backend a été re-tiré) — ForgotPasswordView.vue/ResetPasswordView.vue appellent une URL codée en dur (fetch('http://localhost:5000/api/auth/forgot-password', ...)) au lieu de l'instance api (axios) à baseURL configurable utilisée partout ailleurs.

Voir Authentification & 2FA.

Nouveau cache sur taux de change/publications/réglages site — jamais invalidé​

Le 2026-08-24, un cache Redis a été ajouté sur GET /exchange-rates (10 min), GET /publications (5 min) et GET /site-settings/:key (10 min) — voir Contenu vitrine. Contrairement au cache déjà existant sur GET /slides (qui appelle apicache.clear() à chaque écriture), aucun des trois contrôleurs concernés n'invalide le cache après une création/modification. Toute publication d'un taux de change, d'une publication ou d'un changement de réglage met donc jusqu'à 5-10 minutes à apparaître publiquement, sans aucun moyen de forcer un rafraîchissement.

/api/otp/send et /api/otp/verify sans limiteur de débit dédié​

Mise à jour le 2026-08-24 (second pull) : le backend a réorganisé son rate-limiting global (eb315ff) — un strictLimiter (5 000 requêtes/15 min par IP) s'applique désormais à toutes les routes POST/PUT/DELETE de l'API, contre 10 000 req/15 min toutes méthodes confondues auparavant. C'est mieux, mais toujours pas dédié : contrairement à /api/tracking (trackingLimiter, 15 req/15 min, voir OTP, SMS & vérification SBA), un code à 6 chiffres avec une fenêtre de validité de 5 minutes reste en pratique bruteforçable par numéro de téléphone dans l'enveloppe des 5 000 requêtes, et chaque tentative sur /otp/send déclenche potentiellement un vrai envoi SMS facturé via Kannel et un vrai appel au middleware bancaire UWM. À corriger avant tout trafic public réel sur ces formulaires.

✅ Corrigés (2026-08-13)​

updateLoanStatus n'avait aucune vérification d'appartenance à une agence​

backend/src/controllers/loan.controller.js, fonction updateLoanStatus (PATCH /api/loan-requests/:id) — contrairement aux 3 autres endpoints du contrôleur, celle-ci ne vérifiait jamais que req.user avait le droit d'agir sur le dossier ciblé : tout utilisateur backoffice authentifié pouvait changer le statut de n'importe quelle demande de prêt, y compris celles d'une autre agence — le point le plus sensible du contrôleur puisqu'il exécute la décision (pas juste une consultation). canAccessAgency(req.user, currentLoan.id_agence) ajouté juste après le chargement du dossier.

account_requests : lecture entièrement publique, sans authentification​

backend/src/routes/account_requests.routes.js : GET / et GET /:id n'avaient aucun middleware verifyToken — n'importe qui, sans être connecté, pouvait lister l'intégralité des demandes d'ouverture de compte (identité, téléphone, email, adresse, données FATCA/TIN). verifyToken ajouté sur les deux routes, et agencyScopeWhere/canAccessAgency appliqués à getAll/getById (account_requests.controller.js, account_requests.service.js::getAll accepte désormais un where) — un utilisateur authentifié ne voit maintenant que les demandes de sa propre agence (ou de sa zone pour CHEF_ZONE), cohérent avec account.controller.js et loan.controller.js.

loan_parameters modifiable sans authentification​

PUT /api/loan-parameters/:id exige désormais verifyToken (GET reste public, consommé par les formulaires publics).

Alertes lisibles sans authentification​

GET /api/alertes et GET /api/alertes/:id exigent désormais verifyToken.

POST /api/users accessible à tout utilisateur authentifié​

POST/PUT/DELETE /api/users exigent désormais isSuperAdmin (même middleware que /api/roles).

Le bypass 000000 de la 2FA n'existe pas (mais était documenté comme existant)​

Le README backend et le commentaire dans SchoolLoanView.vue ont été corrigés pour ne plus affirmer l'existence de ce bypass. Le placeholder OTP côté prêt scolaire (000000) reste en place — c'est un mécanisme distinct, volontaire, en attendant l'intégration Sopra Banking Amplitude, voir Demande de prêt scolaire.

Mise à jour de statut d'une demande de compte : route inexistante​

AccountRequestDetailView.vue/AccountRequestsView.vue appellent désormais PATCH /api/account-requests/:id/status (route réellement exposée).

LoanDocumentsConfigView.vue appelait le mauvais endpoint de suppression​

Corrigé : DELETE /loan-document-requirements/:id (au lieu de /loan-requirements/:id).

CNI (signataires/bureau/promoteur) jamais enregistrées comme documents​

OpenAccountView.vue les inclut désormais dans additional_data.documents (mêmes clés descriptives : signataire_N_cni, bureau_{role}_cni, promoteur_cni) — elles passent maintenant par le mécanisme générique déjà utilisé pour les autres pièces, aucun changement backend nécessaire. Voir Ouverture de compte.

Sauvegarde des slides : suppression totale sans transaction​

Un nouvel endpoint PUT /api/slides (slidesService.replaceAll) remplace l'ancienne séquence DELETE/POST multiples non transactionnelle par une seule transaction Prisma (delete + recreate atomiques, éléments inclus). Voir Éditeur de bannières.

URL de setup-password codée en dur​

utils/mailer.js lit désormais process.env.FRONTEND_URL (repli sur http://localhost:5173 si absent) ; FRONTEND_URL ajoutée à .env.example.

loan_field_requirements réservé SUPER_ADMIN, incohérent avec loan_document_requirements​

La restriction isSuperAdmin a été retirée — les deux routers de configuration de prêt exigent maintenant uniformément verifyToken seul.

CHEF_ZONE ne filtrait pas réellement par zone​

Cette entrée a été marquée "corrigée" une première fois à tort

Le module agencyScopeWhere/canAccessAgency (backend/src/utils/agency-scope.js) avait bien été écrit et branché sur loan.controller.js/account.controller.js/account_requests.controller.js, mais restait inopérant pour deux raisons cumulées, découvertes seulement en testant avec un vrai compte CHEF_ZONE (login + 2FA + appels API réels) :

  1. CHEF_ZONE avait la permission view_all_agencies (seed/seed_roles.js) — cette permission court-circuite agencyScopeWhere/canAccessAgency en premier (accès total, sans restriction), rendant la logique "restreindre à la zone" écrite juste en dessous inatteignable. Retirée des permissions de CHEF_ZONE.
  2. Une fois ce point corrigé, plus aucun dossier n'était visible pour CHEF_ZONE (régression inverse) : id_zone n'était jamais inclus dans le JWT émis par verify2fa — seul id_agence l'était. Ajouté au payload JWT et à la sélection Prisma de getMe (auth.controller.js).

Leçon : une correction non testée en conditions réelles (vrai login, pas juste une relecture de code) peut sembler correcte tout en étant totalement inopérante.

Un CHEF_ZONE est désormais réellement restreint aux agences de sa propre zone (id_zone), vérifié par un test de bout en bout (accès accepté sur une agence de sa zone, 403 sur une agence d'une autre zone). Voir Agences, zones & GAB et Authentification & 2FA.

README backend : message de démarrage NLP trompeur​

"NLP Model Trained Successfully with 26 intents" a été retiré — ce message décrivait un ancien moteur NLP (@nlpjs) que le code actuel ne charge plus (le vrai moteur actif est un système RAG + Ollama, désactivé côté serveur par choix, pas un bug — voir Chatbot (NLP + RAG)). Le serveur n'affiche désormais que UTB Backend Server running on port 3000 au démarrage, ce que le README reflète maintenant.

Doc Swagger de users.routes.js obsolète​

Corrigée : password retiré des champs requis/documentés de POST /api/users (jamais utilisé par create(), qui crée le compte sans mot de passe et envoie une invitation) ; two_factor_secret/two_factor_enabled retirés (destructurés mais jamais utilisés) ; id_agence/id_zone ajoutés (réellement pris en compte, absents de la doc précédente).

seed_fields.js — entrée retirée, le fichier n'existe pas​

Une version antérieure de cette page affirmait qu'un ancien script seed_fields.js traînait à la racine du dépôt backend, hérité d'avant la réorganisation des scripts de seed. Vérification faite le 2026-08-13 : ce fichier n'existe pas dans le dépôt actuel — l'affirmation provenait d'un contenu migré depuis l'ancienne doc du projet sans revérification indépendante à ce moment-là. Corrigé ici pour que l'erreur ne se propage pas plus loin.