
CVE-2026-39154, XSS almacenado en CometChat JS SDK
Este directorio contiene una reproducción honesta y autocontenida del sumidero de renderizado
detrás de CVE-2026-39154: un defecto de cross-site scripting almacenado en el
SDK de JavaScript de CometChat 3.0.12, donde el campo data.text de un mensaje de grupo se renderiza en
los clientes de otros miembros sin codificación de salida contextual.
Se ejecuta íntegramente de forma local con datos sintéticos. No contacta, sondea ni se autentica contra el servicio CometChat en vivo ni contra ningún tercero. Es una demostración local únicamente de la ruta de renderizado vulnerable del cliente.
Repositorio PoC correspondiente (mencionado en el writeup): https://github.com/defineid/Wildfire
Writeup: writeup-cometchat-xss.html en este portafolio.
| Campo | Valor |
|---|---|
| CVE | CVE-2026-39154 |
| Proveedor / Producto | CometChat · JavaScript SDK 3.0.12 |
| Clase | Cross-Site Scripting almacenado (CWE-79) |
| Endpoint | POST /v3.0/messages · campo de inyección data.text |
| Receptor | receiverType: group |
| Interacción | Ninguna; el payload almacenado se ejecuta automáticamente cuando un miembro abre el grupo |
| Divulgación | Reportado a CometChat en feb 2026 (solicitud 42660); el proveedor lo marcó como Solved; CVE asignado por MITRE |
| Referencia | https://www.cve.org/CVERecord?id=CVE-2026-39154 |
Nota sobre la versión corregida: el writeup publicado indica que el proveedor resolvió el problema ("VENDOR: SOLVED") pero no nombra un número específico de SDK/build corregido, y no se proporciona ninguna URL de aviso del proveedor aparte del registro CVE anterior. Aquí no se afirma ningún número de versión corregida porque el writeup no lo proporciona.
Un TextMessage de CometChat lleva su texto en data.text, expuesto al código de la UI como
message.getText(). Un miembro autenticado puede enviar, a un grupo, un mensaje cuyo
data.text contiene marcado HTML, por ejemplo:
POST /v3.0/messages
{
"receiverType": "group",
"receiver": "<groupId>",
"category": "message",
"type": "text",
"data": { "text": "" }
}
La plataforma almacena esa cadena tal cual. Cuando cualquier miembro del grupo abre la conversación, la ruta de renderizado de la lista de mensajes del cliente toma data.text y lo inserta en el DOM como HTML, sin codificación de salida contextual (una escritura al estilo innerHTML en lugar de una escritura textContent). Por lo tanto, el marcado `` se analiza; su src=x falla al cargar, lo que dispara el manejador onerror, y el script del atacante se ejecuta en la sesión de la víctima. Debido a que el mensaje se almacena, esto es persistente y no requiere interacción de la víctima más allá de ver el grupo.
La corrección divulgada sanitiza / codifica contextualmente este campo, de modo que la misma entrada se renderiza como texto inerte en lugar de ejecutarse.
Un miembro autenticado del grupo es el atacante; las víctimas son los demás miembros de ese grupo. El payload se entrega una vez, se almacena en el servidor y se ejecuta automáticamente en el navegador de cada miembro cuando visualiza la conversación. No se requiere clic, concesión de permisos ni interacción adicional. Impacto en el informe original: ejecución arbitraria de scripts en la sesión de cada miembro (robo de sesión/token, acciones en nombre de la víctima, divulgación de datos de la conversación).
repro.html — una página local básica (blanco liso, CSS mínimo, un área simple de lista de mensajes) que representa únicamente la ruta de renderizado vulnerable del SDK 3.0.12. Contiene una copia local fiel del sink no sanitizado:
function getText(message){ return message.data.text; } // mirrors TextMessage#getText()
...
body.innerHTML = getText(message); // *** the SDK 3.0.12 sink, reproduced ***
Renderiza un objeto sintético de mensaje almacenado a través de ese sink para que el script inyectado se ejecute localmente. Una nota honesta en línea en la página indica que se trata de una reproducción local del sink del SDK 3.0.12, que el SDK corregido sanitiza el campo, y que no interviene ningún servicio de CometChat en vivo.
payload.json — el objeto sintético almacenado TextMessage. Su campo data.text
es el punto de inyección (anotado con las claves _injection_point / _note, ya que
JSON no admite comentarios). El vector de inyección (``) es
idéntico al del informe divulgado; la acción se cambia por un marcador
inofensivo dentro de la página.
payload.txt — la misma inyección documentada en texto plano, incluida la
solicitud original manipulada, con una nota explícita de que no se envía a ningún sitio.
Este repro.html es un sustituto mínimo escrito a partir del mecanismo descrito en el informe.
No es un extracto del código fuente del proveedor; el SDK real no se obtuvo
ni se desensambló para esta reproducción.
El payload de esta reproducción es benigno: en lugar de alert(document.domain), el
manejador onerror establece window.__xss_fired = true, escribe RESULT: XSS-FIRED en
la página y establece el título del documento. No se exfiltra nada.
cd poc/CVE-2026-39154
python3 -m http.server 5599
# then open http://localhost:5599/repro.html in a browser
repro.html renderiza una copia embebida del mensaje almacenado de forma síncrona (de modo que el sink se dispara de manera determinista incluso en captura headless) y además obtiene payload.json para confirmar que el artefacto en disco se carga y porta el mismo campo.
RESULT: XSS-FIRED en rojo — prueba de que el script onerror inyectado se ejecutó a través del sink innerHTML no saneado.data.text se codificaría y aparecería como el texto literal `` sin ejecución de script.payload.json era JSON bien formado y se confirmó que portaba la inyección en data.text (un vector ). El directorio se sirvió con `python3 -m http.server 5599`, y `repro.html` se cargó en **Firefox 140.11.0esr headless** mediante `--screenshot` (guardado aquí como `repro-screenshot.png`). La captura de pantalla obtenida muestra el renderizado como un elemento de imagen rota (es decir, data.text se analizó como HTML, no se mostró como texto) y RESULT: XSS-FIRED escrito en el DOM. Esto confirma que el sink innerHTML no saneado ejecuta el script inyectado desde el campo del mensaje./v3.0/messages, no se utilizó ninguna cuenta ni grupo, y no se descargó, ejecutó ni desensambló ninguna compilación real del SDK. El sink aquí es un sustituto fiel escrito a mano basado en el mecanismo descrito en el writeup, no el código distribuido por el proveedor. La versión exacta del SDK corregido no se confirmó porque el writeup no indica ninguna.Se trata de una reproducción local y offline que utiliza únicamente datos sintéticos autogenerados, construida para documentar un CVE ya reportado y resuelto por el proveedor. Se dirige a un sustituto local de la ruta de renderizado vulnerable, no a ningún sistema en vivo ni de terceros. El payload es un marcador inofensivo dentro de la página; no contiene exfiltración ni weaponización más allá de demostrar que el campo se renderiza sin sanear.