
सुरक्षा परामर्श: बिना प्रमाणीकरण के संग्रहीत क्रॉस-साइट स्क्रिप्टिंग जिसके कारण व्यवस्थापक खाते का अधिग्रहण (openclaw-dashboard)
शीर्षक: OpenClaw Dashboard v3.0.0 विफल लॉगिन यूज़रनेम फ़ील्ड के माध्यम से स्टोर्ड XSS निर्धारित CVE ID: CVE-2026-66418
लक्ष्य रिपॉज़िटरी: https://github.com/tugcantopaloglu/openclaw-dashboard
लॉगिन एंडपॉइंट प्रस्तुत यूज़रनेम को बिना किसी सत्यापन के ऑडिट लॉग में दर्ज करता है।
नोटिफिकेशन पैनल बाद में उन लॉग प्रविष्टियों को पढ़ता है और उन्हें innerHTML के साथ, बिना किसी एस्केपिंग के, पृष्ठ में लिखता है।
एक हमलावर जो लॉग इन नहीं कर सकता, फिर भी एक विफल लॉगिन अनुरोध भेज सकता है जिसका यूज़रनेम एक स्क्रिप्ट पेलोड हो।
अगली बार जब लॉग-इन एडमिनिस्ट्रेटर नोटिफिकेशन बेल खोलता है, तो वह पेलोड उनके ब्राउज़र में, डैशबोर्ड के ओरिजिन में, उनके सत्र टोकन तक पहुंच के साथ चलता है।
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:N/SC:H/SI:H/SA:Nv3.0.0 में प्रस्तुत किया गया, जिसने नोटिफिकेशन सेंटर जोड़ा जो ऑडिट-लॉग प्रविष्टियों को प्रस्तुत करता है।
v3.0.0 और उसके बाद के हर कमिट में मौजूद है, जिसमें वर्तमान main (d6198d0) भी शामिल है। लेखन के समय इसे ठीक नहीं किया गया है।
हमलावर एक दूरस्थ पक्ष है जो डैशबोर्ड के HTTP पोर्ट तक पहुंच सकता है लेकिन उसके पास न तो कोई खाता है और न ही वैध क्रेडेंशियल। यह इस तथ्य से मेल खाता है कि लॉगिन स्क्रीन किसी भी व्यक्ति के लिए सुलभ है जो डैशबोर्ड खोल सकता है। एकमात्र पूर्व-शर्त यह है कि एक एडमिनिस्ट्रेटर खाता पहले से मौजूद है, जो किसी भी तैनाती के लिए पहली बार सेटअप के बाद सत्य है।
पेलोड स्टोर्ड है, इसलिए किसी समय-समन्वय की आवश्यकता नहीं है। यह तब निष्पादित होता है जब एडमिनिस्ट्रेटर नोटिफिकेशन पैनल खोलता है, जो UI द्वारा प्रदान की जाने वाली एक सामान्य क्रिया है। एक बार चलने के बाद, इसमें एडमिनिस्ट्रेटर के ब्राउज़र के समान क्षमताएं होती हैं: यह सत्र टोकन पढ़ सकता है और पीड़ित की ओर से किसी भी प्रमाणित एंडपॉइंट को कॉल कर सकता है।
चरण 1. यूज़रनेम यथावत लॉग किया जाता है। एक ऐसे यूज़रनेम के साथ लॉगिन जो पंजीकृत खाते से मेल नहीं खाता, इस शाखा तक पहुंचता है:
// server.js:1577-1583
if (username !== creds.username) {
recordFailedAuth(ip);
auditLog('login_failed', ip, { username });
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid username or password' }));
return;
}
username सीधे JSON अनुरोध निकाय से आता है। इसकी कोई लंबाई सीमा, वर्ण अनुमति-सूची, या प्रकार जांच नहीं है। auditLog इसे JSON पंक्ति के रूप में डिस्क पर लिखता है:
// server.js:278-282
function auditLog(event, ip, details = {}) {
try {
const timestamp = new Date().toISOString();
const entry = JSON.stringify({ timestamp, event, ip, ...details }) + '\n';
fs.appendFileSync(auditLogPath, entry, 'utf8');
JSON.stringify उद्धरणों और नई पंक्तियों को एस्केप करता है, इसलिए पेलोड एक पंक्ति में रहता है और साफ-साफ वापस पार्स हो जाता है। यह <, >, या / को एस्केप नहीं करता, इसलिए HTML मार्कअप अक्षुण्ण बना रहता है।
चरण 2. लॉग वापस पढ़ा जाता है। नोटिफिकेशन एंडपॉइंट हाल की लॉग पंक्तियों को ब्राउज़र में लौटाता है:
// server.js:2042-2051
if (req.url.startsWith('/api/notifications')) {
if (!requireAuth(req, res)) return;
const limit = parseInt(new URL(req.url, 'http://localhost').searchParams.get('limit') || '50');
try {
const raw = fs.readFileSync(auditLogPath, 'utf8').trim();
const lines = raw.split('\n').filter(Boolean).slice(-Math.min(limit, 200));
const events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean).reverse();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ events }));
चरण 3. यूज़रनेम बिना एस्केपिंग के DOM में लिखा जाता है। फ्रंटएंड प्रत्येक नोटिफिकेशन पंक्ति को स्ट्रिंग संयोजन द्वारा बनाता है और इसे innerHTML के साथ निर्दिष्ट करता है:
// index.html:5647-5653
body.innerHTML = data.events.map(e => {
const icon = notifIcons[e.event] || '📋';
const time = e.timestamp ? new Date(e.timestamp).toLocaleString() : '';
const detail = e.username ? ' (' + e.username + ')' : '';
const ip = e.ip ? ' from ' + e.ip : '';
return '<div class="notif-item"><div class="notif-icon">' + icon + '</div><div class="notif-content"><div class="notif-event">' + (e.event||'').replace(/_/g, ' ') + detail + ip + '</div><div class="notif-time">' + time + '</div></div></div>';
}).join('');
e.username हमलावर की स्ट्रिंग है। यह बिना किसी एन्कोडिंग के innerHTML तक पहुंचती है, इसलिए ब्राउज़र इसे HTML के रूप में पार्स करता है।
server.js:298 पर सेट की गई Content-Security-Policy में script-src 'self' 'unsafe-inline' शामिल है, इसलिए onerror जैसे इनलाइन इवेंट हैंडलर चलने की अनुमति रखते हैं।
सुनिश्चित करें कि एक एडमिनिस्ट्रेटर खाता मौजूद है (कोई भी सामान्य इंस्टॉल)। हमलावर को उसके क्रेडेंशियल की आवश्यकता नहीं है।
अनप्रमाणित हमलावर के रूप में, एक विफल लॉगिन भेजें जिसका यूज़रनेम पेलोड है:
curl -X POST http://TARGET:7000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"","password":"x"}'
सर्वर 401 Invalid username or password उत्तर देता है और पेलोड संग्रहीत करता है।
एडमिनिस्ट्रेटर सामान्य रूप से लॉग इन करता है और नोटिफिकेशन बेल पर क्लिक करता है।
पेलोड एडमिनिस्ट्रेटर के सत्र में चलता है। इस उदाहरण में यह पृष्ठ के अपने getStoredToken() के साथ सत्र टोकन पढ़ता है और इसका उपयोग POST /api/key-file के माध्यम से एजेंट निर्देश फ़ाइल AGENTS.md को अधिलेखित करने के लिए करता है। किसी भी प्रमाणित एंडपॉइंट को उसी तरह कॉल किया जा सकता है।
पेलोड टेक्स्ट एक एकल लॉग पंक्ति बना रहता है और /api/notifications के माध्यम से बाइट-दर-बाइट वापस आता है, इसलिए इंजेक्शन और निष्पादन की अलग-अलग पुष्टि की जा सकती है।
एडमिनिस्ट्रेटर के रूप में डैशबोर्ड ओरिजिन में कोड निष्पादन। स्क्रिप्ट सत्र टोकन पढ़ सकती है और कोई भी प्रमाणित अनुरोध जारी कर सकती है, जिसमें एजेंट के निर्देश और कौशल फ़ाइलों को संपादित करना और OpenClaw कॉन्फ़िगरेशन बदलना शामिल है। चूंकि हमलावर को किसी खाते की आवश्यकता नहीं है, यह एक अनप्रमाणित नेटवर्क अनुरोध को एडमिनिस्ट्रेटर के सत्र के नियंत्रण में बदल देता है।
innerHTML में रखने से पहले प्रत्येक मान को HTML-एन्कोड करें, या स्ट्रिंग संयोजन के बजाय textContent के साथ नोड बनाएं। नोटिफिकेशन यूज़रनेम, इवेंट और IP फ़ील्ड सभी को इसकी आवश्यकता है।username को मान्य करें: इसकी लंबाई सीमित करें और इसे एक अपेक्षित वर्ण सेट तक प्रतिबंधित करें।script-src से 'unsafe-inline' हटाएं। इनलाइन हैंडलर के अवरुद्ध होने के साथ, यह समस्या कोड निष्पादन से घटकर हानिरहित मार्कअप तक आ जाती है।