
Sicherheitshinweis: Stored Cross-Site Scripting über Agentennachrichten führt zum Diebstahl von Session-Tokens (openclaw-dashboard)
Titel: OpenClaw Dashboard – Gespeichertes XSS über das lastMessage-Sitzungsfeld CVE ID: CVE-2026-66421
https://github.com/tugcantopaloglu/openclaw-dashboard
Die Sitzungstabelle auf der Startseite des Dashboards zeigt die letzte Nachricht jeder Agentensitzung. Dieser Text stammt aus dem Gesprächsprotokoll des Agenten und wird über innerHTML ohne Escape-Behandlung in die Seite geschrieben. OpenClaw ist ein Multi-Channel-Agent-Gateway, daher kann der Nachrichtentext von jeder Person stammen, die mit dem Agenten kommunizieren kann, etwa über eine Chatgruppe oder einen Webhook. Eine Nachricht mit einem HTML-Payload wird als Skript im Browser des Administrators ausgeführt, sobald dieser das Dashboard öffnet.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:H/SI:H/SA:NSeit der ersten öffentlichen Veröffentlichung vorhanden. Die verwundbare Darstellung existiert im frühesten Tag (v1.1.0) und in jedem späteren Commit bis einschließlich des aktuellen main (d6198d0). Zum Zeitpunkt der Erstellung dieses Berichts nicht behoben.
Der Angreifer ist eine Partei, die eine Nachricht senden kann, die im Sitzungsprotokoll des Agenten landet. Er benötigt kein Dashboard-Konto. In einer typischen OpenClaw-Bereitstellung gehören dazu Mitglieder verbundener Chat-Kanäle sowie jede Quelle, die den Agenten über einen Webhook erreicht. Der Angreifer befindet sich daher häufig außerhalb der Vertrauensgrenze des Betreibers.
Der Payload wird im Protokoll gespeichert und immer dann gerendert, wenn der Administrator die Sitzungsseite aufruft. Diese Seite ist die Standard-Startansicht und wird über einen Timer aktualisiert. Die einzige erforderliche Interaktion ist das Laden des Dashboards, das der Administrator ohnehin nutzt. AT:P spiegelt die eine Bedingung wider, auf die der Angreifer angewiesen ist: dass die Bereitstellung seine Nachrichten in eine Sitzung leitet, die das Dashboard anzeigt.
Schritt 1: Der Nachrichtentext wird aus dem Protokoll extrahiert. Der Server liest die neueste Nachricht und gibt deren Text, auf 80 Zeichen gekürzt, ohne Bereinigung zurück:
// 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);
Schritt 2: Er wird über die Sessions-API bereitgestellt. getSessionsJson legt den Text im Feld lastMessage jeder Sitzung ab:
// server.js:538
lastMessage: getLastMessage(s.sessionId || key),
GET /api/sessions gibt dieses Array an den Browser zurück.
Schritt 3: Er wird ohne Escape-Behandlung in das DOM geschrieben. Die Sitzungstabelle erstellt jede Zeile per String-Konkatenation und weist sie über innerHTML zu:
// 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 wird direkt in den HTML-String eingesetzt. Auf diesem Pfad gibt es keinen einzigen Kodierungsschritt.
Dieselbe Tabelle fügt s.label (index.html:3770) und s.key (index.html:3767) auf dieselbe Weise in das Zeilen-Markup ein. Diese Felder haben kein 60-Zeichen-Limit und bieten einem Angreifer, der sie beeinflussen kann, daher mehr Platz für einen Payload.
Die Content-Security-Policy in server.js:298 erlaubt Inline-Event-Handler (script-src 'self' 'unsafe-inline'), sodass ein onerror-Handler ausgeführt wird.
Senden Sie als Partei, die den Agenten benachrichtigen kann, eine Nachricht, deren Text ein kurzer Payload ist, der in das 60-Zeichen-Budget passt, zum Beispiel:
Das sind 56 Zeichen, und es erreicht das Sitzungsprotokoll des Agenten wie jede normale Nachricht.
Bestätigen Sie, dass der Server den Payload uneskepiert zurückgibt:
curl -H "Authorization: Bearer ADMIN_TOKEN" http://TARGET:7000/api/sessions
Das Feld lastMessage der betroffenen Sitzung enthält den rohen ``-String.
Der Administrator öffnet das Dashboard. Die Sitzungsseite ist die Standardansicht, daher ist keine zusätzliche Navigation nötig. Die Zeile wird gerendert, der Browser parst das ``-Tag, der onerror-Handler wird ausgeführt und das Sitzungstoken des Administrators wird vom Payload ausgelesen.
Ersetzt man den console.log-Rumpf durch ein fetch auf einen gleichnamigen (same-origin) authentifizierten Endpunkt, kann der Payload mit den Rechten des angemeldeten Benutzers handeln.
Skriptausführung in der Herkunft des Dashboards als angemeldeter Benutzer, erreichbar durch jemanden, der nur eine Nachricht an den Agenten senden muss. Der Payload kann das Sitzungstoken lesen und authentifizierte Endpunkte aufrufen, einschließlich derjenigen, die die eigenen Anweisungsdateien des Agenten und die OpenClaw-Konfiguration bearbeiten. Da es auf der Standardseite ausgelöst wird, muss der Administrator keine ungewöhnliche Aktion ausführen.
lastMessage, label und key per HTML-Encoding, bevor Sie sie in das Zeilen-Markup einfügen, oder erstellen Sie die Zellen mit textContent statt mit einem HTML-String.innerHTML gerendert werden.'unsafe-inline' aus script-src, damit injiziertes Markup nicht ausgeführt werden kann, selbst wenn ein Kodierungsschritt übersehen wird.