ESC

Type to search the knowledge base.

XSS for Frontend Engineers

How cross-site scripting reaches UI sinks, what React/HTML escaping does and doesn’t save, and layered defenses.

intermediate2 min read
  • security
  • xss
  • web

Cross-site scripting (XSS) means an attacker gets script (or equivalent) to run in your users’ origin. On the frontend, we create most of the sinks — the places untrusted data becomes executable or dangerous markup.

If you only say “React escapes everything,” you’ll miss dangerouslySetInnerHTML, markdown renderers, rich text, URL attributes, and server-built HTML.

Flavors you’ll hear in interviews

Type Story
Stored Payload saved (comment, profile) and served later to others
Reflected Payload in URL/query bounced into the response
DOM-based Client JS reads untrusted input (location, postMessage, …) and writes it into a sink

Same outcome: attacker code in your origin, with your users’ cookies/storage privileges (depending on cookie flags).

Sinks frontend code keeps inventing

// HTML sinks
el.innerHTML = userInput;
el.outerHTML = userInput;
document.write(userInput);

// Often safer for pure text
el.textContent = userInput;

// URL / navigation sinks
location.href = userInput;
a.href = userInput; // javascript: URLs still surprise people

Framework defaults help when you stay in the happy path:

// React escapes text children
<p>{userInput}</p>

// You opted out of safety
<div dangerouslySetInnerHTML={{ __html: userInput }} />

Markdown → HTML, PDF preview, chart tooltips, email clients… every “we render user HTML” feature needs a real sanitizer policy, not hope.

Defense in depth (stack them)

  1. Don’t treat untrusted data as HTML. Prefer text nodes / escaped templates.
  2. Sanitize only when rich HTML is a product requirement — use a maintained library and a strict allowlist.
  3. CSP — reduce blast radius (script-src with nonces/hashes; avoid unsafe-inline when you can).
  4. Cookie flagsHttpOnly, Secure, SameSite for session cookies so stolen JS can’t always read the session.
  5. Validate URLs — block javascript: and unexpected schemes on href / src.

No single control is enough. CSP without safe sinks still fails; safe sinks without CSP still fail open on the next bug.

A DOM XSS path you can narrate

  1. Source: location.hash or query param.
  2. Flow: read in JS, maybe “decode” it.
  3. Sink: assign to innerHTML or build HTML with string concat.
  4. Fix: textContent, or sanitize, or stop reflecting raw input.

That four-step story scores better than a laundry list of acronyms.

Further reading

Related guides