CVE-2025-22381: Host Header Injection in Aggie Detailed analysis and Proof-of-Concept for CVE-2025-22381, a Host Header Injection vulnerability discovered in the Aggie Open-Source Project. ************************* ************************* Vulnerability Overview+ ************************* ************************* CVE ID: CVE-2025-22381 Published: October 2025 (MITRE assignment) Disclosed publicly: February 2026 Reporter: Anas Abderrahman Benbarek Discovery Date: September 17, 2025 Affected Project: TID-Lab/aggie Affected Versions: All versions (including 2.6.1 and earlier; no fix applied as of February 2026) Severity: Medium to High (estimated CVSS ~7.1–7.5) Impact: Enables phishing attacks leading to password reset token theft and potential account takeover. ************************* ************************* Background ************************* ************************* I spend a fair amount of time reviewing open source node.js projects on GitHub, especially ones that handle authentication flows In September 2025 while looking through the Aggie repository, I noticed something that immediately stood out in the pasword reset logic. What started as routine code reading ended up becoming CVE-2025-22381 — a classic Host Header Injection vulnerability that allows an attacker to control the domain in pasword reset emails. ************************* ************************* How I Found It ************************* ************************* i cloned the repository and started reading files under lib/api/, focusing on anything related to authentication and email generation. The file lib/api/reset-password.js contains the endpoint logic for /reset-password. The critical part is inside the sendEmail helper: 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); } The line host: req.headers.host is the problem. In express, req.headers.host comes directly from the Host HTTP header, which is entirely attacker-controlled. There is no validation, no whitelist, no fallback to a trusted domain from configuration. ************************* ************************* Initial Confirmation ************************* ************************* I quickly set up a local instance following the README instructions (Ubuntu, nvm, npm install, secrets.json with test SMTP), started the server, and trigered a pasword reset. The generated email link used localhost:3000 as expected. Then I replayed the request with a manipulated Host header: curl -X POST http://localhost:3000/reset-password \ -H "Host: evil-phish.example" \ -d "email=test@victim.com" The email (captured via MailHog) contained: http://evil-phish.example/reset-password?token=... Proof positive. The application trusts the client-supplied Host header when building the reset link. ************************* ************************* How the Attack Actually Works ************************* ************************* Attacker sends a password reset request for a victim s email address, but sets the Host header to a domain they control (e.g. evil-phish.example). Aggie generates a legitimate reset token (server-side, time-limited, encrypted with the config secret). The email is sent containing a link to the attacker s domain instead of the real one. Victim receives the email and clicks the link (phishing success condition). The victim lands on the attacker s server. The attacker s server can: Simply display a fake “reset failed” page and silently discard the token, or Capture the token from the query string (via server-side logging or JavaScript), or Proxy the request to the real Aggie instance, capture the token, and forward the user to the legitimate reset page (so the victim doesn t immediately notice anything wrong). Attacker later uses the captured token on the real domain to reset the victim s pasword. The key point: Host header injection alone does not let the attacker use the token directly. The attacker still needs the victim to visit the malicious link so the token reaches the attacker s infrastructure. That s why this is a phishing-enabling vulnerability rather than a direct account takeover without user interaction. ************************* ************************* Technical Severity & Impact ************************* ************************* This is a medium-to-high severity issue depending on context: AV:N: Network reachable PR:N: No privileges required AC:L: Low complexity UI:R: Requires user interaction S:C: Scope can change (impact extends to the victim’s account on the legitimate domain) C:L / I:H: Confidentiality & Integrity impact on the victim’s account Many databases list it with CVSS ~7.1–7.5 range I personally consider it serious in production environments where Aggie is used for sensitive monitoring (elections, crises), as successful phishing here can lead to full account takeover. ************************* ************************* Proof-of-Concept (Detailed & Reproducible) ************************* ************************* Environment Ubuntu 18.04/20.04 (as recommended) Node 12.16 (per .nvmrc) MailHog: Running locally for email capture (docker run -d -p 8025:8025 -p 1025:1025 mailhog/mailhog) Aggie configured with email.transport pointing to localhost:1025 Step-by-step Clone & start 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 Create a test user via the web UI or directly in MongoDB. Trigger malicious reset: curl -i -X POST http://localhost:3000/reset-password \ -H "Host: evil-phish.example" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "email=test@victim.com" Open MailHog: Inspect the sent email. The reset link will point to http://evil-phish.example/reset-password?token=... ************************* ************************* Disclosure Timeline ************************* ************************* Sep 17, 2025: Discovered + local PoC Sep 17, 2025: Emailed mikeb@cc.gatech.edu with full details and PoC Sep 17, 2025: Submitted to MITRE (service request 1926730 / MCID15453119) Oct 9, 2025: MITRE assigned CVE-2025-22381 Oct–Dec 2025: No public patch or response observed Feb 2026: Public disclosure (this article) ************************* ************************* Recommended Fix ************************* ************************* Code Change Replace the vulnerable line with a trusted value in 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 Add to secrets.json: "appHost": "[https://your-real-domain.com](https://your-real-domain.com)" ************************* ************************* Closing Thoughts ************************* ************************* host header injection remains surprisingly common in 2025–2026 especially in projects that were started years ago and haven’t been heavily audited. Aggie is a valuable tool for civic tech and crisis monitoring — I hope the maintainers apply a fix soon. If you maintain or use Aggie check your deployment and patch manually until an official release lands. Feel free to reach out if you have questions or want to discuss similar issues in other projects. thanks for reading and stay safe out there.