
タイトル: OpenClawダッシュボードのlastMessageセッションフィールドを介した保存型XSS 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)と、現在の main(d6198d0)までのすべての以降のコミットに存在します。執筆時点では修正されていません。
攻撃者は、エージェントのセッションのトランスクリプトに格納されるメッセージを送信できる者です。ダッシュボードのアカウントは不要です。一般的な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 の Content-Security-Policy はインラインイベントハンドラを許可しているため(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 をHTMLエンコードするか、HTML文字列ではなく textContent でセルを構築してください。innerHTML を介してレンダリングされる他のセッションフィールドにも同じエンコードを適用してください。script-src から 'unsafe-inline' を削除して、たとえエンコード手順が欠落しても、挿入されたマークアップが実行されないようにしてください。