# XSS Sweep: Escape Server-Controlled Data at `innerHTML` Boundaries Priority: **Critical** · Effort: **Low** · Risk: **Low** ## Problem The client injects server-controlled strings into the DOM via `innerHTML` **without escaping** in several high-traffic render paths. Because the server broadcasts channel names, listener usernames, toast messages, and track titles over WebSocket to *every* connected client, a single malicious payload is a **stored/reflected XSS propagated peer-to-peer**. There is also **no Content-Security-Policy** header set on the document or by the server, so once injected, script runs with full page privilege (same-origin as the session cookie). ## Affected Locations | File:Line | Sink | Source | |-----------|------|--------| | `public/channelSync.js:174-183` | channel list `${ch.name}` and `` | channel name (server) | | `public/channelSync.js:161-163` | `listenersHtml` (listener usernames) | `ch.listeners` (server) | | `public/utils.js:113` | toast history `... ${item.message}` | WS `toast` message (server) | | `public/utils.js:149,159` | track title marquee | `track.title` (file metadata / yt-dlp) | | `public/queue.js:342` | now-playing bar `${title}` | `track.title` (server) | | `public/upload.js:307` | slow-queue list `${group.name}` | playlist name (server) | | `public/upload.js:330` | slow-queue list `${item.title}` | `/api/fetch` response (server) | Note: `trackComponent.js:59` and `playlists.js:44,57,58` **do** escape correctly today. The codebase is internally inconsistent — those are the model to follow. ## Root Cause - No single shared escaping utility. `escapeHtml` is defined **twice** (`public/playlists.js:481-486` and `public/trackComponent.js:74-79`) as local copies. - No lint rule or review guard preventing raw `${serverData}` inside template literals feeding `innerHTML`. - No CSP as defense-in-depth. ## Implementation Plan ### Step 1 — Create one shared `escapeHtml` in `public/utils.js` Move/deduplicate the existing helper into `utils.js` and expose it on `M`: ```js M.escapeHtml = function (str) { if (str == null) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }; ``` Delete the two local copies in `playlists.js:481` and `trackComponent.js:74`; replace callers with `M.escapeHtml(...)`. ### Step 2 — Escape every server-sourced value at each sink above For each location, wrap the interpolated value in `M.escapeHtml(...)`. For attribute contexts (e.g. `value="${...}"`), escaping with the helper above is sufficient since it includes `"`. Worked example for `channelSync.js:174-183`: ```js div.innerHTML = `