
CVE-2026-39154, XSS stocké dans le SDK JS CometChat
Ce répertoire contient une reproduction honnête et autonome du sink de rendu
à l'origine de CVE-2026-39154 : une faille de cross-site scripting stocké dans le
SDK JavaScript CometChat 3.0.12, où le champ data.text d'un message de groupe est rendu dans
les clients des autres membres sans encodage contextuel de la sortie.
Il s'exécute entièrement en local avec des données synthétiques. Il n'entre pas en contact, ne sonde pas et ne s'authentifie pas auprès du service CometChat en production ni d'aucun tiers. Il s'agit d'une démonstration locale du chemin de rendu client vulnérable uniquement.
Dépôt PoC correspondant (cité dans l'analyse) : https://github.com/defineid/Wildfire
Analyse : writeup-cometchat-xss.html dans ce portfolio.
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-39154 |
| Éditeur / Produit | CometChat · JavaScript SDK 3.0.12 |
| Classe | Cross-Site Scripting stocké (CWE-79) |
| Point de terminaison | POST /v3.0/messages · champ d'injection data.text |
| Destinataire | receiverType: group |
| Interaction | Aucune ; la charge utile stockée s'exécute automatiquement lorsqu'un membre ouvre le groupe |
| Divulgation | Signalé à CometChat en février 2026 (demande 42660) ; l'éditeur a marqué Solved ; CVE attribué par MITRE |
| Référence | https://www.cve.org/CVERecord?id=CVE-2026-39154 |
Remarque sur la version corrigée : l'analyse publiée indique que l'éditeur a résolu le problème (« VENDOR: SOLVED ») mais ne nomme pas de numéro spécifique de SDK/build corrigé, et aucune URL d'avis éditeur distincte n'est fournie au-delà de l'enregistrement CVE ci-dessus. Aucun numéro de version corrigée n'est affirmé ici car l'analyse n'en fournit pas.
Un TextMessage CometChat transporte son texte dans data.text, exposé au code de l'interface sous la forme
message.getText(). Un membre authentifié peut envoyer, à un groupe, un message dont le
data.text contient du balisage HTML, par exemple :
POST /v3.0/messages
{
"receiverType": "group",
"receiver": "<groupId>",
"category": "message",
"type": "text",
"data": { "text": "" }
}
La plateforme stocke cette chaîne telle quelle. Lorsqu'un membre du groupe ouvre la
conversation, le chemin de rendu de la liste des messages du client prend data.text et l'insère
dans le DOM en tant que HTML, sans encodage contextuel de sortie (une écriture de type innerHTML
plutôt qu'une écriture textContent). Le balisage `` est donc analysé ;
son src=x échoue à se charger, ce qui déclenche le gestionnaire onerror, et le script de l'attaquant s'exécute dans la session de la victime. Comme le message est stocké, il s'agit d'une attaque persistante qui ne nécessite aucune interaction de la victime au-delà de la consultation du groupe.
Le correctif divulgué assainit / encode contextuellement ce champ, de sorte que la même entrée s'affiche comme du texte inerte au lieu de s'exécuter.
Un membre authentifié du groupe est l'attaquant ; les victimes sont les autres membres de ce groupe. La charge utile est livrée une fois, stockée côté serveur, et s'exécute automatiquement dans le navigateur de chaque membre lorsqu'il consulte la conversation. Aucun clic, aucune autorisation, ni aucune interaction supplémentaire n'est requise. Impact dans le rapport d'origine : exécution de script arbitraire dans la session de chaque membre (vol de session/jeton, actions au nom de la victime, divulgation des données de la conversation).
repro.html — une page locale brute (fond blanc uni, CSS minimal, une simple zone de liste de messages)
qui représente uniquement le chemin de rendu vulnérable du SDK 3.0.12. Elle contient
une copie locale fidèle du puits non assaini :
function getText(message){ return message.data.text; } // mirrors TextMessage#getText()
...
body.innerHTML = getText(message); // *** the SDK 3.0.12 sink, reproduced ***
Il rend un objet de message stocké synthétique via ce sink afin que le script injecté s'exécute localement. Une note honnête intégrée à la page indique qu'il s'agit d'une reproduction locale du sink du SDK 3.0.12, que le SDK corrigé assainit le champ, et qu'aucun service CometChat en direct n'est impliqué.
payload.json — l'objet TextMessage stocké synthétique. Son champ data.text est le point d'injection (annoté avec les clés _injection_point / _note, puisque JSON ne prend pas en charge les commentaires). Le vecteur d'injection (``) est identique à celui du rapport divulgué ; l'action est remplacée par un marqueur inoffensif dans la page.
payload.txt — la même injection documentée en texte brut, incluant la requête forgée d'origine, avec une note explicite indiquant qu'elle n'est envoyée nulle part.
Ce repro.html est un substitut minimal écrit à partir du mécanisme décrit dans l'analyse. Il ne s'agit pas d'un extrait du code source de l'éditeur ; le SDK réel n'a pas été obtenu ni désassemblé pour cette reproduction.
La charge utile de cette repro est bénigne : au lieu de alert(document.domain), le gestionnaire onerror définit window.__xss_fired = true, écrit RESULT: XSS-FIRED dans la page et définit le titre du document. Rien n'est exfiltré.
cd poc/CVE-2026-39154
python3 -m http.server 5599
# then open http://localhost:5599/repro.html in a browser
repro.html effectue le rendu synchrone d'une copie embarquée du message stocké (afin que le sink se déclenche de manière déterministe, même en capture headless) et récupère en outre payload.json pour confirmer que l'artefact sur disque se charge et porte le même champ.
RESULT: XSS-FIRED en rouge — preuve que le script onerror injecté s'est exécuté via le sink innerHTML non assaini.data.text serait encodé et apparaîtrait comme le texte littéral `` sans exécution de script.payload.json a été validé comme JSON bien formé et confirmé comme portant l'injection dans data.text (un vecteur ). Le répertoire a été servi avec `python3 -m http.server 5599`, et `repro.html` a été chargé dans **Firefox 140.11.0esr headless** via `--screenshot` (enregistré ici sous `repro-screenshot.png`). La capture d'écran obtenue montre le rendu comme un élément d'image cassée (c'est-à-dire que data.text a été interprété comme du HTML, et non affiché comme du texte) et RESULT: XSS-FIRED écrit dans le DOM. Cela confirme que le sink innerHTML non assaini exécute le script injecté depuis le champ du message./v3.0/messages, aucun compte ni groupe n'a été utilisé, et aucune version réelle du SDK n'a été téléchargée, exécutée ou désassemblée. Le sink présent ici est un substitut fidèle écrit à la main, fondé sur le mécanisme décrit dans l'analyse, et non sur le code livré par l'éditeur. La version exacte du SDK corrigé n'a pas été confirmée, car l'analyse n'en indique aucune.