QuestionsNetworking/Security

Cross-site scripting (XSS): types and defenses

Web VulnerabilitiesMediumNetworking/Security

Explain the types of XSS and the layered defenses. Why is escaping context-dependent, and where does CSP fit?

What it tests

Understanding the web's most pervasive vulnerability class end-to-end: how injection happens, the three variants, and the defense-in-depth stack.

Approach & answer

XSS is when an attacker gets their JavaScript to run in a victim's browser IN YOUR ORIGIN'S CONTEXT — which, because of the same-origin policy, means that script inherits full access to your page: it can read the DOM, steal non-HttpOnly cookies and tokens, make authenticated requests as the user, keylog, and rewrite the page. There are three types. STORED (persistent) XSS: the payload is saved on the server (a comment, a profile field) and served to every viewer — the most dangerous because it's wormable and hits many users. REFLECTED XSS: the payload rides in the request (a query param, form field) and is echoed straight back into the response, so the attacker must lure the victim to a crafted URL. DOM-BASED XSS: the vulnerability is entirely client-side — JS reads attacker-controlled input (location.hash, a URL param) and writes it into a sink like innerHTML or eval without sanitizing, so the payload may never touch the server. The core defense is CONTEXT-AWARE OUTPUT ENCODING: escape untrusted data for the exact place it lands, because the rules differ — HTML body context needs &lt; &gt; &amp; escaping; an HTML attribute needs attribute-encoding and quoting; inside a <script> or a URL or a CSS context the rules change again, and HTML-escaping alone won't save you. In practice: prefer APIs that don't parse HTML — textContent, setAttribute, and framework text bindings ({value} in React, which auto-escapes) treat input as data, not markup. AVOID the dangerous sinks: innerHTML, outerHTML, document.write, eval, and React's dangerouslySetInnerHTML. When you genuinely must render user-supplied HTML (a rich-text field), run it through a vetted SANITIZER (DOMPurify) with an allowlist — never a hand-rolled blocklist. Then layer defenses that limit the blast radius even if something slips through: a Content Security Policy that forbids inline scripts and restricts script sources turns many injections into no-ops; HttpOnly cookies keep session tokens unreadable by injected script; Trusted Types (where supported) make dangerous sinks refuse raw strings. No single layer is sufficient — escaping is the primary control, CSP and HttpOnly are the safety net, and a sanitizer covers the deliberate-HTML case.

Use this technique when

Reviewing code that renders user input; designing an XSS defense strategy; explaining why a sink is dangerous.

Code

// DANGEROUS — parses the string as HTML, executes injected script
el.innerHTML = userInput;                 // stored/reflected/DOM XSS sink
container.insertAdjacentHTML('beforeend', userInput);
element.setAttribute('onclick', userInput);
// React equivalent:
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// SAFE — treat input as DATA, not markup
el.textContent = userInput;               // no parsing, no execution
el.setAttribute('title', userInput);      // attribute value, encoded
<div>{userInput}</div>                     // React auto-escapes

// Must render real HTML? Sanitize with an allowlist:
el.innerHTML = DOMPurify.sanitize(userHtml);

// Defense in depth (server response header):
//   Content-Security-Policy: default-src 'self'; script-src 'self'
//   -> inline & injected scripts won't run even if one slips through
// Plus: HttpOnly cookies so injected JS can't read the session token.

References