[security] Stored XSS in the dashboard — unescaped API/request data rendered via innerHTML #23
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: high. Live stored XSS, one vector reachable by an unauthenticated remote attacker.
Problem
dashboard.htmlbuilds five views by interpolating server data straight intoinnerHTMLwith no escaping:${l.request_path}and${l.request_method}— captured from actual proxied traffic. An attacker sends a request through the proxy to any logging-enabled service with a path like/<img src=x onerror=…>; it's stored inrequest_logs; when an admin opens the dashboard, it executes in the admin's authenticated session. No credentials required — just the ability to hit a logged service.${s.name},${s.target_host},${s.routing_hosts},${r.match_sni}, etc. — registerable by any API-key holder (including scoped service keys). Authenticated stored XSS → admin session.Impact
XSS runs with the admin's session (post-#22 the cookie is httpOnly, so it can't be exfiltrated — but the script can ride the session): deregister services, rewrite TLS domains, register malicious routes, read logs. Effectively full admin takeover triggered by viewing the dashboard.
Fix
innerHTML(anesc()HTML-escaper applied to every data${…}, or build nodes viatextContent/DOM APIs instead of string templates). This is the direct fix and is small.<script>andonclick=handlers, so a strictscript-src(nounsafe-inline) needs those moved to a nonce'd/external script and inline handlers removed.Priority
Pre-existing (not introduced by the session-auth work), but it's the top residual security risk — above passkeys (step 2). The session-cookie change limited XSS exfiltration; it did not remove the XSS itself. Recommend fixing the escaping next, CSP soon after. Surfaced 2026-08-10.
Fixed in
086de00.Two-layer remediation:
Output escaping (the fix). Introduced an auto-escaping
htmltagged-template helper — every interpolated value is HTML-escaped by default, so injected markup renders as inert text. All five render functions (services, L4 routes, static sites, request logs) now go through it. The one deliberately-trusted fragment ("logging on") uses an explicitraw()opt-out. Escaping is now the default, so future templates are safe without per-sink discipline.Content-Security-Policy (defense-in-depth). The dashboard is now served with a strict per-request policy:
default-src 'none'; script-src 'nonce-<random>'; style-src 'unsafe-inline'; img-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'. Script runs only if it carries that response's fresh random nonce, so any escape that slips through in future can't execute. Required nonce-ing the inline<script>and converting the two inlineonclick=handlers toaddEventListener— the page now has zero inline script. Also setsX-Content-Type-Options: nosniffandReferrer-Policy: no-referrer.Tests added: nonce injection + policy assertions (no
script-src 'unsafe-inline'/'unsafe-eval') and per-response nonce uniqueness. Full suite green (182), clippy + fmt clean.