Challenge 3: Identify a Stored XSS Vector — Possible Solution ==================================================================== THE VULNERABLE CODE --------------------- socket.addEventListener("message", (event) => { const data = JSON.parse(event.data); const messageDiv = document.createElement("div"); messageDiv.innerHTML = data.text; // <-- the vulnerability chatWindow.appendChild(messageDiv); }); HOW THIS BECOMES STORED XSS ------------------------------ The XSS course's core lesson was that browsers don't distinguish "text the developer wrote" from "text a user supplied" once both end up inside an HTML-parsing context — innerHTML always parses its argument AS HTML, never as plain text, regardless of where that string originally came from. Here's the concrete attack path: 1. A malicious (or compromised) client sends a chat message where "text" is something like: 2. That message travels over the WebSocket exactly like any other chat message — nothing about the transport (WebSocket vs a normal HTTP form post) makes this payload any different from ordinary text data. 3. The room's server-side broadcast logic (Chapter 6) forwards this message to every OTHER client currently in the room, exactly as it would forward any legitimate chat message — the server has no reason to treat this text differently unless it's specifically validating or sanitizing message content (which Challenge 2's validateChatMessage only checks length/type, not HTML content). 4. Every recipient's client runs the vulnerable code above: messageDiv.innerHTML = data.text parses the malicious string AS HTML, creating a real element with a broken src — which immediately fires its onerror handler, executing the attacker's JavaScript in the context of every other user's browser session, with access to their cookies, DOM, and whatever else that origin's JavaScript can normally do. Why this is specifically "stored" (rather than merely "reflected") XSS: the malicious payload isn't tied to one single request/response — once broadcast, EVERY client who is or later joins that room and receives this message (or a replayed/logged version of it) re-executes the payload, the same defining trait of stored XSS the XSS course covered for database-backed content, just persisted in "whoever's still connected to this room" instead of a database row. THE FIX -------- Replace innerHTML with textContent when rendering plain chat text: messageDiv.textContent = data.text; textContent inserts the string as literal text — a browser will display the literal characters "" on screen rather than parsing and executing them as an HTML element. If the chat feature genuinely needs to support some rich formatting (bold, links, etc.), the safe approach is running the untrusted text through a proper sanitizer library (such as DOMPurify, as the XSS course covered) that strips dangerous elements/attributes while preserving an allowlisted set of safe formatting — never rendering raw, unsanitized user input with innerHTML.