
Avviso di Sicurezza: Cross-Site Scripting Memorizzato Non Autenticato che Porta all'Acquisizione dell'Account Amministratore (openclaw-dashboard)
Titolo: OpenClaw Dashboard v3.0.0 Stored XSS tramite il campo Username del login fallito ID CVE assegnato: CVE-2026-66418
Repository target: https://github.com/tugcantopaloglu/openclaw-dashboard
L'endpoint di login registra lo username inviato nel log di audit senza alcuna
validazione. Il pannello delle notifiche legge successivamente quelle voci di log e le
scrive nella pagina con innerHTML e senza escape. Un attaccante che non può accedere
può comunque inviare una richiesta di login fallita il cui username è un payload di
script. La volta successiva in cui l'amministratore autenticato apre la campanella
delle notifiche, il payload viene eseguito nel suo browser, nell'origine della
dashboard, con accesso al suo token di sessione.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:N/SC:H/SI:H/SA:NIntrodotta nella v3.0.0, che ha aggiunto il centro notifiche che visualizza le voci del log di audit. Presente nella v3.0.0 e in ogni commit successivo fino a includere l'attuale main (d6198d0). Non risolta al momento della stesura.
L'attaccante è un soggetto remoto che può raggiungere la porta HTTP della dashboard ma non possiede un account né credenziali valide. Ciò corrisponde alla schermata di login raggiungibile da chiunque possa aprire la dashboard. L'unica precondizione è che esista già un account amministratore, il che è vero per qualsiasi distribuzione oltre la configurazione iniziale.
Il payload è persistente, quindi non è necessaria alcuna coordinazione temporale. Viene eseguito quando l'amministratore apre il pannello delle notifiche, un'azione normale offerta dall'interfaccia. Una volta eseguito, ha le stesse capacità del browser dell'amministratore: può leggere il token di sessione e chiamare qualsiasi endpoint autenticato per conto della vittima.
Passaggio 1. Lo username viene registrato nei log così com'è. Un login con uno username che non corrisponde all'account registrato raggiunge questo ramo:
// 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 arriva direttamente dal corpo della richiesta JSON. Non esiste alcun limite di lunghezza, allowlist di caratteri o controllo del tipo. auditLog lo scrive su disco come riga 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 esegue l'escape di virgolette e newline, quindi il payload rimane su una sola riga e viene rieseguito in parsing correttamente. Non esegue l'escape di <, > o /, quindi il markup HTML sopravvive intatto.
Passaggio 2. Il log viene riletto. L'endpoint delle notifiche restituisce le righe di log recenti al browser:
// 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 }));
Passaggio 3. Lo username viene scritto nel DOM senza escape. Il frontend costruisce ogni riga di notifica tramite concatenazione di stringhe e la assegna con 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 è la stringa dell'attaccante. Raggiunge innerHTML senza alcuna codifica, quindi il browser la interpreta come HTML.
La Content-Security-Policy impostata in server.js:298 include script-src 'self' 'unsafe-inline', quindi i gestori di eventi inline come onerror possono essere eseguiti.
Assicurarsi che esista un account amministratore (qualsiasi installazione normale). L'attaccante non ha bisogno delle sue credenziali.
In qualità di attaccante non autenticato, inviare un login fallito il cui username è il payload:
curl -X POST http://TARGET:7000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"","password":"x"}'
L'amministratore accede normalmente e fa clic sulla campanella delle notifiche.
Il payload viene eseguito nella sessione dell'amministratore. In questo esempio legge
il token di sessione con il getStoredToken() della pagina e lo usa per sovrascrivere
il file di istruzioni dell'agente AGENTS.md tramite POST /api/key-file. Qualsiasi
endpoint autenticato può essere chiamato allo stesso modo.
Il testo del payload rimane su una singola riga di log e torna byte per byte tramite /api/notifications, quindi l'iniezione e l'esecuzione possono essere confermate separatamente.
Esecuzione di codice nell'origine della dashboard con i privilegi dell'amministratore. Lo script può leggere il token di sessione e inviare qualsiasi richiesta autenticata, inclusa la modifica dei file di istruzioni e delle skill dell'agente e la modifica della configurazione di OpenClaw. Poiché l'attaccante non ha bisogno di un account, una richiesta di rete non autenticata si trasforma nel controllo della sessione dell'amministratore.
innerHTML, oppure costruire i
nodi con textContent invece della concatenazione di stringhe. I campi username,
evento e IP delle notifiche richiedono tutti questo trattamento.username sul server prima di registrarlo nei log: limitarne la lunghezza e
restringerlo a un set di caratteri previsto.'unsafe-inline' da script-src. Con i gestori inline bloccati, questo
problema scende da esecuzione di codice a markup innocuo.