
Уведомление о безопасности: хранимый межсайтовый скриптинг через сообщения агента, приводящий к краже токена сессии (openclaw-dashboard)
Заголовок: Хранимый XSS в OpenClaw Dashboard через поле сессии lastMessage CVE ID: CVE-2026-66421
https://github.com/tugcantopaloglu/openclaw-dashboard
Таблица сессий на целевой странице панели управления показывает последнее сообщение каждой
сессии агента. Этот текст берётся из стенограммы разговора агента и записывается на
страницу через innerHTML без экранирования. OpenClaw — это многоканальный шлюз для агентов,
поэтому текст сообщения может поступать от любого, кто способен общаться с агентом, например
из чат-группы или через вебхук. Сообщение, содержащее HTML-пейлоад, выполняется как скрипт в
браузере администратора, как только тот открывает панель управления.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:H/SI:H/SA:NПрисутствует с первого публичного релиза. Уязвимый рендер существует в самом раннем теге (v1.1.0) и во всех последующих коммитах вплоть до текущего (d6198d0). На момент написания не исправлено.
mainЗлоумышленник — сторона, которая может отправить сообщение, попадающее в стенограмму сессии агента. Ему не нужна учётная запись панели управления. В типичном развёртывании OpenClaw к таким сторонам относятся участники подключённых чат-каналов и любой источник, который обращается к агенту через вебхук, поэтому злоумышленник часто находится за пределами доверительной границы оператора.
Пейлоад хранится в стенограмме и рендерится при каждом просмотре администратором страницы сессий.
Эта страница является представлением по умолчанию и обновляется по таймеру, поэтому единственное
требуемое взаимодействие — загрузка администратором панели управления, которой он обычно пользуется.
AT:P отражает единственное условие, на которое полагается злоумышленник: что развёртывание
направляет его сообщения в сессию, которую отображает панель.
Шаг 1. Текст сообщения извлекается из стенограммы. Сервер читает самое последнее сообщение и возвращает его текст, обрезанный до 80 символов, без санитизации:
// 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);
Шаг 2. Он раскрывается через API сессий. getSessionsJson помещает текст в
поле lastMessage каждой сессии:
// server.js:538
lastMessage: getLastMessage(s.sessionId || key),
GET /api/sessions возвращает этот массив в браузер.
Шаг 3. Он записывается в DOM без экранирования. Таблица сессий строит каждую
строку конкатенацией строк и присваивает её через 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 помещается прямо в HTML-строку. На этом пути нет ни одного шага кодирования.
Та же таблица интерполирует s.label (index.html:3770) и s.key (index.html:3767)
в разметку строки тем же способом. Эти поля не имеют ограничения в 60 символов, поэтому
злоумышленник, который может на них влиять, получает больше места для пейлоада.
Политика безопасности контента в server.js:298 разрешает встроенные обработчики событий
(script-src 'self' 'unsafe-inline'), поэтому обработчик onerror выполняется.
Будучи стороной, которая может отправлять сообщения агенту, отправьте сообщение, текст которого представляет собой короткий пейлоад, укладывающийся в бюджет 60 символов, например:
Это 56 символов, и оно попадает в стенограмму сессии агента как любое обычное сообщение.
Подтвердите, что сервер возвращает пейлоад без экранирования:
curl -H "Authorization: Bearer ADMIN_TOKEN" http://TARGET:7000/api/sessions
Поле lastMessage затронутой сессии содержит исходную строку ``.
Администратор открывает панель управления. Страница сессий является представлением по
умолчанию, поэтому дополнительная навигация не требуется. Строка рендерится, браузер
разбирает тег ``, обработчик onerror выполняется, и токен сессии администратора
считывается пейлоадом.
Замена тела console.log на fetch к аутентифицированной конечной точке того же источника позволяет пейлоаду
действовать с привилегиями вошедшего в систему пользователя.
Выполнение скрипта в источнике панели управления от имени вошедшего пользователя, доступное тому, кому достаточно просто отправить агенту сообщение. Пейлоад может прочитать токен сессии и вызывать аутентифицированные конечные точки, включая те, которые редактируют файлы инструкций самого агента и конфигурацию OpenClaw. Поскольку срабатывание происходит на странице по умолчанию, администратору не нужно предпринимать никаких необычных действий.
lastMessage, label и key перед вставкой в разметку строки,
или создавайте ячейки через textContent, а не HTML-строку.innerHTML.'unsafe-inline' из script-src, чтобы внедрённая разметка не могла выполниться,
даже если шаг кодирования будет пропущен.