Level 2 · 25 min
XSS: Stored, Reflected, and DOM-based
Cross-Site Scripting (XSS) is what happens when attacker-controlled text reaches the browser as HTML, JS, or another active context. The attacker''s code runs in the victim''s session — same-origin — so it can read cookies (if not HttpOnly), call any API the victim can, and exfiltrate the result. The defense is contextual output encoding plus a Content-Security-Policy that limits damage even if encoding fails.
Three Variants You Will See
Stored XSS — the payload is persisted (a comment, a profile field, a Markdown post) and runs every time another user views it. Highest impact: one payload, all viewers compromised. Reflected XSS — the payload is in the URL or form submission and rendered back without persistence: search?q='<'script'>'... is the textbook example. The attacker tricks the victim into clicking the link. DOM-based XSS — the bug is in client-side JavaScript: document.body.innerHTML = location.hash.slice(1). The server never sees the payload; defense must happen in the browser. A particularly nasty variant is stored XSS in Markdown renderers — many Markdown libraries allow inline HTML, and an unsafe configuration that does not strip '<'script'>' or javascript: hrefs makes every comment or wiki page a vector.
Contextual Output Encoding
There is no single 'escape' function that works everywhere. The encoding depends on where the value is interpolated. HTML body — encode < > & to < > &. HTML attribute — also encode '"' to " and prefer attributes always quoted. JavaScript string literal — the rules are different: < must be \u003c (so </script> inside a JS string can not break out), and quote and backslash. URL component — encodeURIComponent. CSS — only allow allowlisted constants. Modern frameworks do most of this for you (React's JSX text nodes are encoded; Angular's {'{value}'} is encoded), but the escape hatches are land mines: dangerouslySetInnerHTML in React, [innerHTML] in Angular, v-html in Vue. Templates that build URLs dynamically (href={url}) need URL validation: reject javascript: and data: schemes.
CSP and Cookie Flags as Defense in Depth
Even the best encoding misses bugs eventually. Content-Security-Policy is a header that tells the browser which sources of scripts/styles/images are allowed. A strict policy: Content-Security-Policy: default-src ''self''; script-src ''self'' ''nonce-AbCdEf123''; object-src ''none''; base-uri ''self''. The nonce is regenerated per response and added to every '<'script'>' the server emits — '<'script'>'s without the matching nonce do not execute, even if injected. Avoid ''unsafe-inline'' and ''unsafe-eval''. Cookies should always be Secure (HTTPS only), HttpOnly (no JS access — kills the cookie-theft path of XSS), and SameSite=Lax or Strict (also helps with CSRF). HSTS (Strict-Transport-Security: max-age=31536000; includeSubDomains; preload) prevents protocol downgrade. None of these are a substitute for not having XSS — they limit the blast radius when you do.
Code example
// VULNERABLE — innerHTML with user input
document.getElementById("out").innerHTML = userInput;
// FIXED — textContent (browsers escape automatically)
document.getElementById("out").textContent = userInput;
// VULNERABLE — JSP with no escaping
<div>Welcome <%= request.getParameter("name") %>'</div>
// FIXED — JSTL <c:out> escapes by default
<div>Welcome <c:out value="${param.name}" />'</div>
// CSP header (nonce regenerated per response)
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'nonce-" + nonce + "'; " +
"object-src 'none'; base-uri 'self'");
// <script nonce="${nonce}">...</script> // executes
// <script>evil()</script> // blocked