
Discovering CVE-2025-22381: Host Header Injection in the Aggie Open-Source Project
CVE-2025-22381 : Injection d'en-tête Host dans Aggie
Analyse détaillée et Preuve de Concept pour CVE-2025-22381, une vulnérabilité d'injection d'en-tête Host découverte dans le projet open-source Aggie.
Aperçu de la vulnérabilité+
ID CVE : CVE-2025-22381
Publié : Octobre 2025 (attribution MITRE)
Divulgué publiquement : Février 2026
Signaleur : Anas Abderrahman Benbarek
Date de découverte : 17 septembre 2025
Projet affecté : TID-Lab/aggie
Versions affectées : Toutes les versions (y compris 2.6.1 et antérieures ; aucun correctif appliqué en février 2026)
Gravité : Moyenne à Élevée (CVSS estimé ~7.1–7.5)
Impact : Permet des attaques de phishing conduisant au vol du jeton de réinitialisation de mot de passe et à une possible prise de contrôle du compte.
Contexte
Je consacre pas mal de temps à examiner des projets Node.js open source sur GitHub, en particulier ceux qui gèrent des flux d'authentification. En septembre 2025, en parcourant le dépôt Aggie, j'ai remarqué quelque chose qui ressortait immédiatement dans la logique de réinitialisation de mot de passe. Ce qui a commencé comme une lecture de code de routine a fini par devenir CVE-2025-22381 — une vulnérabilité classique d'injection d'en-tête Host qui permet à un attaquant de contrôler le domaine dans les emails de réinitialisation de mot de passe.
Comment je l'ai trouvé
J'ai cloné le dépôt et commencé à lire les fichiers sous lib/api/, en me concentrant sur tout ce qui concerne l'authentification et la génération d'emails.
Le fichier lib/api/reset-password.js contient la logique du point d'accès pour /reset-password. La partie critique se trouve dans l'assistant sendEmail :
function sendEmail(user, req, callback) {
var token = encodeToken(user);
mailer.sendFromTemplate({
template: 'forgotPassword',
user: user,
token: token,
host: req.headers.host, // ← vulnerable
protocol: req.protocol,
acceptLanguage: req.headers['accept-language']
}, callback);
}
La ligne host: req.headers.host est le problème. Dans Express, req.headers.host provient directement de l'en-tête HTTP Host, qui est entièrement contrôlé par l'attaquant. Il n'y a aucune validation, aucune liste blanche, aucun repli vers un domaine de confiance depuis la configuration.
Confirmation initiale
J'ai rapidement configuré une instance locale en suivant les instructions du README (Ubuntu, nvm, npm install, secrets.json avec SMTP de test), démarré le serveur et déclenché une réinitialisation de mot de passe. Le lien de l'email généré utilisait localhost:3000 comme prévu.
Puis j'ai rejoué la requête avec un en-tête Host manipulé :
curl -X POST http://localhost:3000/reset-password \
-H "Host: evil-phish.example" \
-d "[email protected]"
L'email (capturé via MailHog) contenait : http://evil-phish.example/reset-password?token=...
Preuve positive. L'application fait confiance à l'en-tête Host fourni par le client lors de la construction du lien de réinitialisation.
Comment l'attaque fonctionne réellement
L'attaquant envoie une demande de réinitialisation de mot de passe pour l'adresse email d'une victime, mais définit l'en-tête Host sur un domaine qu'il contrôle (par exemple evil-phish.example).
Aggie génère un jeton de réinitialisation légitime (côté serveur, limité dans le temps, crypté avec le secret de configuration).
L'email est envoyé contenant un lien vers le domaine de l'attaquant au lieu du vrai domaine.
La victime reçoit l'email et clique sur le lien (condition de succès du phishing).
La victime atterrit sur le serveur de l'attaquant.
Le serveur de l'attaquant peut :
Simplement afficher une fausse page « échec de réinitialisation » et jeter silencieusement le jeton, ou
Capturer le jeton depuis la chaîne de requête (via la journalisation côté serveur ou JavaScript), ou
Faire un proxy de la demande vers l'instance Aggie réelle, capturer le jeton et rediriger l'utilisateur vers la page de réinitialisation légitime (afin que la victime ne remarque rien immédiatement).
L'attaquant utilise ensuite le jeton capturé sur le vrai domaine pour réinitialiser le mot de passe de la victime.
Le point clé : l'injection d'en-tête Host seule ne permet pas à l'attaquant d'utiliser le jeton directement. L'attaquant a toujours besoin que la victime visite le lien malveillant pour que le jeton atteigne l'infrastructure de l'attaquant. C'est pourquoi il s'agit d'une vulnérabilité permettant le phishing plutôt qu'une prise de contrôle directe du compte sans interaction de l'utilisateur.
Gravité technique et impact
Il s'agit d'un problème de gravité moyenne à élevée selon le contexte :
De nombreuses bases de données le listent avec une plage CVSS ~7.1–7.5. Je le considère personnellement comme grave dans les environnements de production où Aggie est utilisé pour une surveillance sensible (élections, crises), car un phishing réussi ici peut conduire à une prise de contrôle totale du compte.
Preuve de Concept (détaillée et reproductible)
Environnement
Étape par étape
Clonez et démarrez Aggie :
git clone [https://github.com/TID-Lab/aggie.git](https://github.com/TID-Lab/aggie.git)
cd aggie
nvm install
npm install
cp config/secrets.json.example config/secrets.json
# edit secrets.json → set adminPassword, add test SMTP if needed
npm start
Créez un utilisateur de test via l'interface web ou directement dans MongoDB.
Déclenchez une réinitialisation malveillante :
curl -i -X POST http://localhost:3000/reset-password \
-H "Host: evil-phish.example" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "[email protected]"
Ouvrez MailHog : Inspectez l'email envoyé. Le lien de réinitialisation pointera vers http://evil-phish.example/reset-password?token=...
Chronologie de la divulgation
Correctif recommandé
Modification du code
Remplacez la ligne vulnérable par une valeur de confiance dans lib/api/reset-password.js :
// In lib/api/reset-password.js, inside sendEmail()
const config = require('../../config/secrets').get();
// Option A: Hard trust config value (recommended for single-domain)
const host = config.appHost || 'localhost:3000';
// Then use it:
mailer.sendFromTemplate({
template: 'forgotPassword',
user: user,
token: token,
host: host, // Use the trusted variable
protocol: config.environment === 'production' ? 'https' : req.protocol,
acceptLanguage: req.headers['accept-language']
}, callback);
Configuration
Ajoutez à secrets.json :
"appHost": "https://your-real-domain.com"
Réflexions finales
L'injection d'en-tête Host reste étonnamment courante en 2025-2026, en particulier dans les projets démarrés il y a des années et qui n'ont pas été lourdement audités. Aggie est un outil précieux pour la tech civique et la surveillance des crises — j'espère que les mainteneurs appliqueront un correctif bientôt.
Si vous maintenez ou utilisez Aggie, vérifiez votre déploiement et corrigez manuellement jusqu'à ce qu'une version officielle sorte. N'hésitez pas à me contacter si vous avez des questions ou souhaitez discuter de problèmes similaires dans d'autres projets.
Merci d'avoir lu et restez prudents.