
Avviso di sicurezza: Cross-Site Scripting persistente tramite messaggi dell'agente che porta al furto del token di sessione (openclaw-dashboard)
Titolo: XSS Memorizzato nella Dashboard OpenClaw tramite il campo di sessione lastMessage ID CVE: CVE-2026-66421
https://github.com/tugcantopaloglu/openclaw-dashboard
La tabella delle sessioni nella pagina iniziale della dashboard mostra l'ultimo messaggio di ogni sessione agente. Quel testo viene preso dalla trascrizione della conversazione dell'agente e scritto nella pagina con innerHTML e senza escaping. OpenClaw è un gateway multi-canale per agenti, quindi il testo del messaggio può provenire da chiunque sia in grado di parlare con l'agente, ad esempio un gruppo di chat o un webhook. Un messaggio contenente un payload HTML viene eseguito come script nel browser dell'amministratore non appena apre la dashboard.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:H/SI:H/SA:NPresente fin dalla prima release pubblica. Il rendering vulnerabile esiste nel primo tag (v1.1.0) e in ogni commit successivo fino al main attuale incluso (d6198d0). Non corretto al momento della scrittura.
L'attaccante è un soggetto che può inviare un messaggio che finisce nella trascrizione della sessione dell'agente. Non ha bisogno di un account sulla dashboard. In una tipica distribuzione OpenClaw, questo include i membri dei canali chat collegati e qualsiasi fonte che raggiunge l'agente tramite un webhook, quindi l'attaccante è spesso al di fuori del perimetro di fiducia dell'operatore.
Il payload viene memorizzato nella trascrizione e renderizzato ogni volta che l'amministratore visualizza la pagina delle sessioni. Questa pagina è la visualizzazione predefinita e si aggiorna con un timer, quindi l'unica interazione richiesta è che l'amministratore carichi la dashboard che usa normalmente. AT:P riflette l'unica condizione su cui l'attaccante fa affidamento: che la distribuzione instradi i suoi messaggi in una sessione visualizzata dalla dashboard.
Passo 1. Il testo del messaggio viene estratto dalla trascrizione. Il server legge il messaggio più recente e restituisce il suo testo, tagliato a 80 caratteri, senza alcuna sanificazione:
// server.js:474-481
if (typeof msg.content === 'string') {
text = msg.content;
} else if (Array.isArray(msg.content)) {
for (const b of msg.content) {
if (b.type === 'text' && b.text) { text = b.text; break; }
}
}
if (text) return text.replace(/\n/g, ' ').substring(0, 80);
Passo 2. Viene esposto tramite l'API delle sessioni. getSessionsJson inserisce il testo nel campo lastMessage di ogni sessione:
// server.js:538
lastMessage: getLastMessage(s.sessionId || key),
GET /api/sessions restituisce questo array al browser.
Passo 3. Viene scritto nel DOM senza escaping. La tabella delle sessioni costruisce ogni riga mediante concatenazione di stringhe e la assegna con innerHTML:
// index.html:3758
const lastMsg = s.lastMessage ? s.lastMessage.substring(0, 60) + (s.lastMessage.length > 60 ? '…' : '') : '';
// index.html:3773
<div class="table-cell" style="..." onclick="toggleSessionExpand('${escapedKey}', event)">${lastMsg}</div>
lastMsg viene inserito direttamente nella stringa HTML. Non c'è alcun passaggio di codifica in questo percorso.
La stessa tabella interpola s.label (index.html:3770) e s.key (index.html:3767) nel markup della riga allo stesso modo. Questi campi non hanno un limite di 60 caratteri, quindi offrono a un attaccante che può influenzarli più spazio per un payload.
La Content-Security-Policy in server.js:298 consente gestori di eventi inline (script-src 'self' 'unsafe-inline'), quindi un gestore onerror viene eseguito.
Come soggetto che può inviare messaggi all'agente, invia un messaggio il cui testo sia un payload breve che rientra nel budget di 60 caratteri, ad esempio:
Conferma che il server restituisce il payload senza escaping:
curl -H "Authorization: Bearer ADMIN_TOKEN" http://TARGET:7000/api/sessions
Il campo lastMessage della sessione interessata contiene la stringa `` grezza.
L'amministratore apre la dashboard. La pagina delle sessioni è la visualizzazione predefinita, quindi non è necessaria alcuna navigazione aggiuntiva. La riga viene renderizzata, il browser analizza il tag ``, il gestore onerror viene eseguito e il token di sessione dell'amministratore viene letto dal payload.
Sostituendo il corpo di console.log con una fetch verso un endpoint autenticato same-origin, il payload può agire con i privilegi dell'utente connesso.
Esecuzione di script nell'origine della dashboard come utente connesso, raggiungibile da chiunque debba solo inviare un messaggio all'agente. Il payload può leggere il token di sessione e chiamare endpoint autenticati, inclusi quelli che modificano i file di istruzioni dell'agente e la configurazione di OpenClaw. Poiché si attiva nella pagina predefinita, l'amministratore non deve compiere alcuna azione inconsueta.
lastMessage, label e key prima di inserirli nel markup della riga, oppure costruire le celle con textContent anziché con una stringa HTML.innerHTML.'unsafe-inline' da script-src in modo che il markup iniettato non possa essere eseguito anche se un passaggio di codifica viene dimenticato.