
Aviso de seguridad: Cross-Site Scripting Almacenado a través de mensajes de agente que conduce al robo de tokens de sesión (openclaw-dashboard)
Título: OpenClaw Dashboard XSS Almacenado a través del campo lastMessage de la sesión ID de CVE: CVE-2026-66421
https://github.com/tugcantopaloglu/openclaw-dashboard
La tabla de sesiones en la página de inicio del panel muestra el último mensaje de cada sesión de agente.
Ese texto se toma de la transcripción de conversación del agente y se escribe en
la página con innerHTML y sin escape. OpenClaw es una puerta de enlace de agentes multicanal, por lo que
el texto del mensaje puede provenir de cualquier persona que pueda hablar con el agente, como un grupo
de chat o un webhook. Un mensaje que contenga un payload HTML se ejecuta como script en el
navegador del administrador tan pronto como este abre el panel.
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 desde la primera versión pública. El renderizado vulnerable existe en la etiqueta
más antigua (v1.1.0) y en cada commit posterior hasta e incluyendo el main actual
(d6198d0). No corregido en el momento de redactar este aviso.
El atacante es una parte que puede enviar un mensaje que termina en la transcripción de sesión del agente. No necesita una cuenta en el panel. En una implementación típica de OpenClaw, esto incluye miembros de canales de chat conectados y cualquier fuente que llegue al agente a través de un webhook, por lo que el atacante suele estar fuera del límite de confianza del operador.
El payload se almacena en la transcripción y se renderiza cada vez que el administrador ve
la página de sesiones. Esa página es la vista de inicio predeterminada y se actualiza con un temporizador, por lo
que la única interacción requerida es que el administrador cargue el panel que normalmente usa.
AT:P refleja la única condición de la que depende el atacante: que la implementación
enrute sus mensajes hacia una sesión que el panel muestre.
Paso 1. El texto del mensaje se extrae de la transcripción. El servidor lee el mensaje más reciente y devuelve su texto, recortado a 80 caracteres, sin saneamiento:
// 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);
Paso 2. Se expone a través de la API de sesiones. getSessionsJson coloca el texto en el
campo lastMessage de cada sesión:
// server.js:538
lastMessage: getLastMessage(s.sessionId || key),
GET /api/sessions devuelve este array al navegador.
Paso 3. Se escribe en el DOM sin escape. La tabla de sesiones construye cada
fila mediante concatenación de cadenas y la asigna 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 se coloca directamente en la cadena HTML. No hay ningún paso de codificación en ningún punto de
esta ruta.
La misma tabla interpola s.label (index.html:3770) y s.key (index.html:3767)
en el marcado de la fila de la misma manera. Esos campos no tienen límite de 60 caracteres, por lo que dan a un
atacante que pueda influir en ellos más espacio para un payload.
La Política de Seguridad de Contenido en server.js:298 permite manejadores de eventos en línea
(script-src 'self' 'unsafe-inline'), por lo que un manejador onerror se ejecuta.
Como parte que puede enviar mensajes al agente, envíe un mensaje cuyo texto sea un payload corto que quepa en el presupuesto de 60 caracteres, por ejemplo:
Esto son 56 caracteres y llega a la transcripción de sesión del agente como cualquier mensaje normal.
Confirme que el servidor devuelve el payload sin escapar:
curl -H "Authorization: Bearer ADMIN_TOKEN" http://TARGET:7000/api/sessions
El campo lastMessage de la sesión afectada contiene la cadena `` cruda.
El administrador abre el panel. La página de sesiones es la vista predeterminada, por lo que no
se necesita navegación adicional. La fila se renderiza, el navegador analiza la etiqueta ``, el
manejador onerror se ejecuta y el token de sesión del administrador es leído por el payload.
Reemplazar el cuerpo de console.log con un fetch a un endpoint autenticado de mismo origen permite que el payload actúe
con los privilegios del usuario conectado.
Ejecución de scripts en el origen del panel como el usuario conectado, alcanzable por alguien que solo necesita enviar un mensaje al agente. El payload puede leer el token de sesión y llamar a endpoints autenticados, incluidos aquellos que editan los archivos de instrucciones del propio agente y la configuración de OpenClaw. Debido a que se activa en la página predeterminada, el administrador no tiene que realizar ninguna acción inusual.
lastMessage, label y key antes de insertarlos en el marcado
de la fila, o construya las celdas con textContent en lugar de una cadena HTML.innerHTML.'unsafe-inline' de script-src para que el marcado inyectado no pueda ejecutarse incluso si se omite
un paso de codificación.