Command Palette

Search for a command to run...

EN·ES

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 &lt; &gt; &amp;. HTML attribute — also encode '"' to &quot; 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.

Key Takeaways

  • Stored XSS is the worst because one payload affects every viewer. Markdown renderers and rich-text editors are common sources.
  • Encoding is contextual: HTML body, attribute, JS, URL, and CSS each need different rules. There is no universal escape function.
  • CSP with a per-response nonce is the strongest defense in depth — it kills inline-script injection even when encoding fails.

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