
Aviso de Segurança: Cross-Site Scripting Armazenado Através de Mensagens de Agente Levando ao Roubo de Token de Sessão (openclaw-dashboard)
Título: XSS Armazenado no OpenClaw Dashboard via Campo de Sessão lastMessage ID do CVE: CVE-2026-66421
https://github.com/tugcantopaloglu/openclaw-dashboard
A tabela de sessões na página inicial do dashboard mostra a última mensagem de cada sessão de agente. Esse texto é retirado da transcrição da conversa do agente e escrito na página com innerHTML e sem escape. OpenClaw é um gateway de agentes multicanal, então o texto da mensagem pode vir de qualquer pessoa que consiga falar com o agente, como um grupo de chat ou um webhook. Uma mensagem contendo um payload HTML é executada como script no navegador do administrador assim que ele abre o 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 desde a primeira versão pública. A renderização vulnerável existe na tag mais antiga (v1.1.0) e em todos os commits posteriores até e incluindo o atual (d6198d0). Não corrigida no momento em que este texto foi escrito.
mainO atacante é uma parte que pode enviar uma mensagem que cai na transcrição da sessão do agente. Ele não precisa de uma conta no dashboard. Em uma implantação típica do OpenClaw, isso inclui membros de canais de chat conectados e qualquer fonte que alcance o agente por meio de um webhook, portanto o atacante geralmente está fora do limite de confiança do operador.
O payload é armazenado na transcrição e renderizado sempre que o administrador visualiza a página de sessões. Essa página é a visualização padrão de destino e é atualizada por um temporizador, então a única interação necessária é o administrador carregar o dashboard que normalmente usa. AT:P reflete a única condição da qual o atacante depende: que a implantação roteie as mensagens dele para uma sessão que o dashboard exibe.
Passo 1. O texto da mensagem é extraído da transcrição. O servidor lê a mensagem mais recente e retorna seu texto, cortado em 80 caracteres, sem sanitização:
// 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. Ele é exposto pela API de sessões. getSessionsJson coloca o texto no campo lastMessage de cada sessão:
// server.js:538
lastMessage: getLastMessage(s.sessionId || key),
GET /api/sessions retorna esse array para o navegador.
Passo 3. Ele é escrito no DOM sem escape. A tabela de sessões constrói cada linha por concatenação de strings e a atribui com 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 é colocado diretamente na string HTML. Não há nenhuma etapa de codificação em nenhum ponto desse caminho.
A mesma tabela interpola s.label (index.html:3770) e s.key (index.html:3767) na marcação da linha da mesma forma. Esses campos não têm limite de 60 caracteres, então dão a um atacante que possa influenciá-los mais espaço para um payload.
A Content-Security-Policy em server.js:298 permite manipuladores de eventos inline (script-src 'self' 'unsafe-inline'), então um manipulador onerror é executado.
Como uma parte que pode enviar mensagens ao agente, envie uma mensagem cujo texto seja um payload curto que caiba no orçamento de 60 caracteres, por exemplo:
Isso tem 56 caracteres e chega à transcrição da sessão do agente como qualquer mensagem normal.
Confirme que o servidor devolve o payload sem escape:
curl -H "Authorization: Bearer ADMIN_TOKEN" http://TARGET:7000/api/sessions
O campo lastMessage da sessão afetada contém a string `` bruta.
O administrador abre o dashboard. A página de sessões é a visualização padrão, então nenhuma navegação extra é necessária. A linha é renderizada, o navegador analisa a tag ``, o manipulador onerror é executado e o token de sessão do administrador é lido pelo payload.
Substituir o corpo do console.log por um fetch para um endpoint autenticado de mesma origem permite que o payload aja com os privilégios do usuário conectado.
Execução de script na origem do dashboard como o usuário conectado, alcançável por alguém que só precisa enviar uma mensagem ao agente. O payload pode ler o token de sessão e chamar endpoints autenticados, inclusive aqueles que editam os arquivos de instrução do próprio agente e a configuração do OpenClaw. Como ele é disparado na página padrão, o administrador não precisa tomar nenhuma ação incomum.
lastMessage, label e key antes de inseri-los na marcação da linha, ou construa as células com textContent em vez de uma string HTML.innerHTML.'unsafe-inline' de script-src para que a marcação injetada não possa ser executada mesmo que uma etapa de codificação seja esquecida.