theopaid/CVE-2026-66421-OpenClaw-Dashboard-Stored-XSS-via-lastMessage-Session-Field
GitHub: theopaid/CVE-2026-66421-OpenClaw-Dashboard-Stored-XSS-via-lastMessage-Session-Field
该仓库是 OpenClaw Dashboard 存储型 XSS 漏洞(CVE-2026-66421)的安全通告,详细分析了通过 agent 消息注入实现 session token 窃取的攻击路径与修复方案。
Stars: 0 | Forks: 0
# 安全通告:通过 Agent 消息引发的存储型跨站脚本攻击导致 Session Token 窃取 (openclaw-dashboard)
标题:OpenClaw Dashboard 通过 lastMessage Session 字段引发的存储型 XSS
CVE ID:CVE-2026-66421
https://github.com/tugcantopaloglu/openclaw-dashboard
## 概述
Dashboard 首页上的 sessions 表显示了每个 agent
session 的最后一条消息。该文本取自 agent 的对话记录,并通过 `innerHTML`
在未经转义的情况下写入页面。OpenClaw 是一个多渠道 agent 网关,因此
消息文本可以来自任何能够与 agent 对话的人,例如聊天
群组或 webhook。包含 HTML payload 的消息会在
管理员打开 dashboard 时立即在其浏览器中作为脚本运行。
- CWE-79:网页生成时对输入的不正确中和(跨站脚本攻击)
- CVSS 4.0:8.8(高)。向量:`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`
## 受影响版本
自首次公开发布以来便已存在。该漏洞渲染存在于最早的
tag (v1.1.0) 以及随后的每一次 commit 中,直至并包含当前的 `main`
(d6198d0)。在撰写本文时尚未修复。
## 威胁模型
攻击者是能够发送消息并使其进入 agent session 记录的一方。
他们不需要拥有 Dashboard 账号。在典型的 OpenClaw 部署中,这
包括已连接的聊天频道的成员,以及任何通过
webhook 到达 agent 的来源,因此攻击者通常处于运营者的信任边界之外。
payload 被存储在记录中,并在管理员查看
sessions 页面时被渲染。该页面是默认的登录视图,并且会定时刷新,
因此所需的唯一交互就是管理员加载他们通常使用的
dashboard。`AT:P` 反映了攻击者所依赖的一个条件:即部署环境
将他们的消息路由到了 Dashboard 显示的 session 中。
## 根本原因
**第 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 步。它通过 sessions API 暴露。** `getSessionsJson` 将文本放入
每个 session 的 `lastMessage` 字段中:
```
// server.js:538
lastMessage: getLastMessage(s.sessionId || key),
```
`GET /api/sessions` 将此数组返回给浏览器。
**第 3 步。它在未经转义的情况下被写入 DOM。** sessions 表通过
字符串拼接构建每一行,并使用 `innerHTML` 进行赋值:
```
// index.html:3758
const lastMsg = s.lastMessage ? s.lastMessage.substring(0, 60) + (s.lastMessage.length > 60 ? '…' : '') : '';
```
```
// index.html:3773
这是 56 个字符,会像任何正常
消息一样到达 agent 的 session 记录。
2. 确认服务器未经转义便将 payload 返回:
curl -H "Authorization: Bearer ADMIN_TOKEN" http://TARGET:7000/api/sessions
受影响 session 的 `lastMessage` 字段包含原始的 `
` 字符串。
3. 管理员打开 Dashboard。sessions 页面是默认视图,因此不需要
额外的导航。行被渲染,浏览器解析 `
` 标签,
`onerror` 处理程序运行,管理员的 session token 被 payload 读取。
将 `console.log` 函数体替换为向同源已验证 endpoint 发起的 `fetch`,可让 payload 以
已登录用户的权限执行操作。
## 影响
以已登录用户身份在 Dashboard 源中执行脚本,且只需向
agent 发送一条消息即可触达。payload 可以读取 session token 并调用
已验证的 endpoint,包括那些用于编辑 agent 自身指令文件和
OpenClaw 配置的 endpoint。因为它在默认页面上触发,管理员
不必采取任何异常操作。
## 修复建议
- 在将 `lastMessage`、`label` 和 `key` 插入行
markup 之前对其进行 HTML 编码,或者使用 `textContent` 而不是 HTML 字符串来构建单元格。
- 对通过 `innerHTML` 渲染的其他 session 字段应用相同的编码。
- 从 `script-src` 中移除 `'unsafe-inline'`,这样即使漏掉了
某个编码步骤,注入的 markup 也无法执行。
${lastMsg}
```
`lastMsg` 被直接放入 HTML 字符串中。在此路径的任何地方都没有
编码步骤。
同一个表以相同的方式将 `s.label` (index.html:3770) 和 `s.key` (index.html:3767) 插入到行
markup 中。这些字段没有 60 个字符的限制,因此能够影响它们的
攻击者有更多的空间来放置 payload。
server.js:298 处的 Content-Security-Policy 允许内联事件处理程序
(`script-src 'self' 'unsafe-inline'`),因此 `onerror` 处理程序可以运行。
## 概念验证
1. 作为可以向 agent 发送消息的一方,发送一条文本为简短 payload 的消息
(该 payload 需符合 60 个字符的预算限制),例如:
标签:Agent网关, MITM代理, Web前端, XSS漏洞, 多模态安全, 安全公告, 数据可视化, 管理面板