
Sicherheitshinweis: Nicht authentifiziertes, gespeichertes Cross-Site Scripting führt zur Übernahme von Administrator-Konten (openclaw-dashboard)
Titel: OpenClaw Dashboard v3.0.0 – Gespeichertes XSS über das Benutzernamenfeld bei fehlgeschlagenem Login Zugewiesene CVE-ID: CVE-2026-66418
Ziel-Repository: https://github.com/tugcantopaloglu/openclaw-dashboard
Der Login-Endpunkt zeichnet den übermittelten Benutzernamen ohne jegliche Validierung im
Audit-Log auf. Das Benachrichtigungsfeld liest diese Logeinträge später aus und schreibt
sie mit innerHTML und ohne Escaping in die Seite. Ein Angreifer, der sich nicht anmelden
kann, kann dennoch eine fehlgeschlagene Login-Anfrage senden, deren Benutzername ein
Skript-Payload ist. Beim nächsten Öffnen der Benachrichtigungsglocke durch den
angemeldeten Administrator wird dieser Payload im Browser des Administrators ausgeführt –
im Ursprung des Dashboards und mit Zugriff auf dessen Sitzungstoken.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:N/SC:H/SI:H/SA:NEingeführt in v3.0.0, das das Benachrichtigungszentrum hinzufügte, das Audit-Log-Einträge
rendert. Vorhanden in v3.0.0 und in jedem späteren Commit bis einschließlich des
aktuellen main (d6198d0). Zum Zeitpunkt des Schreibens nicht behoben.
Der Angreifer ist eine entfernte Partei, die den HTTP-Port des Dashboards erreichen kann, aber weder ein Konto noch gültige Anmeldedaten besitzt. Dies entspricht der Tatsache, dass der Login-Bildschirm für jeden erreichbar ist, der das Dashboard öffnen kann. Die einzige Voraussetzung ist, dass bereits ein Administrator-Konto existiert, was für jede Bereitstellung nach der Ersteinrichtung gilt.
Der Payload wird gespeichert, daher ist keine zeitliche Koordination erforderlich. Er wird ausgeführt, wenn der Administrator das Benachrichtigungsfeld öffnet, eine normale, von der UI angebotene Aktion. Sobald er läuft, besitzt er dieselben Fähigkeiten wie der Browser des Administrators: Er kann das Sitzungstoken lesen und jeden authentifizierten Endpunkt im Namen des Opfers aufrufen.
Schritt 1: Der Benutzername wird wörtlich protokolliert. Ein Login mit einem Benutzernamen, der nicht mit dem registrierten Konto übereinstimmt, erreicht diesen Zweig:
// 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 stammt direkt aus dem JSON-Anfragetext. Es gibt keine Längenbegrenzung, keine
Zeichen-Allowlist und keine Typüberprüfung. auditLog schreibt den Eintrag als
JSON-Zeile auf die Festplatte:
// 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 maskiert Anführungszeichen und Zeilenumbrüche, sodass der Payload in
einer Zeile bleibt und sich sauber zurückparsen lässt. Es maskiert jedoch weder <, >
noch /, sodass HTML-Markup unversehrt erhalten bleibt.
Schritt 2: Das Log wird zurückgelesen. Der Notifications-Endpunkt gibt die letzten Logzeilen an den Browser zurück:
// 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 }));
Schritt 3: Der Benutzername wird ohne Escaping in das DOM geschrieben. Das Frontend
erstellt jede Benachrichtigungszeile per String-Konkatenation und weist sie über
innerHTML zu:
// 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 ist die Zeichenkette des Angreifers. Sie gelangt ohne Kodierung zu
innerHTML, sodass der Browser sie als HTML parst.
Die in server.js:298 festgelegte Content-Security-Policy enthält script-src 'self' 'unsafe-inline', sodass Inline-Ereignishandler wie onerror ausgeführt werden dürfen.
Stellen Sie sicher, dass ein Administrator-Konto vorhanden ist (bei jeder normalen Installation). Der Angreifer benötigt dessen Anmeldedaten nicht.
Senden Sie als nicht authentifizierter Angreifer einen fehlgeschlagenen Login, dessen Benutzername der Payload ist:
curl -X POST http://TARGET:7000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"","password":"x"}'
Der Server antwortet mit 401 Invalid username or password und speichert den Payload.
Der Administrator meldet sich normal an und klickt auf die Benachrichtigungsglocke.
Der Payload läuft in der Sitzung des Administrators. In diesem Beispiel liest er das
Sitzungstoken mit der eigenen getStoredToken()-Methode der Seite und verwendet es,
um die Agenten-Anweisungsdatei AGENTS.md über POST /api/key-file zu überschreiben.
Jeder authentifizierte Endpunkt kann auf dieselbe Weise aufgerufen werden.
Der Payload-Text bleibt eine einzelne Logzeile und kommt über /api/notifications
Byte für Byte zurück, sodass Injektion und Ausführung getrennt bestätigt werden können.
Codeausführung im Ursprung des Dashboards mit den Rechten des Administrators. Das Skript kann das Sitzungstoken lesen und beliebige authentifizierte Anfragen stellen, einschließlich der Bearbeitung der Anweisungs- und Fähigkeitsdateien des Agents und der Änderung der OpenClaw-Konfiguration. Da der Angreifer kein Konto benötigt, wird aus einer nicht authentifizierten Netzwerkanfrage die Kontrolle über die Sitzung des Administrators.
innerHTML einfügen, oder erstellen
Sie Knoten mit textContent anstelle von String-Konkatenation. Die
Benachrichtigungsfelder für Benutzername, Ereignis und IP benötigen dies alle.username auf dem Server, bevor Sie ihn protokollieren: Begrenzen Sie
seine Länge und beschränken Sie ihn auf einen erwarteten Zeichensatz.'unsafe-inline' aus script-src. Wenn Inline-Handler blockiert sind,
reduziert sich dieses Problem von Codeausführung auf harmloses Markup.