FSD Frontend System Design

Part II β€” The Shield Β· Lesson 3.2

Cross-site Scripting (XSS)

In one lineUntrusted text becomes executable code, and that code owns the user's session.

Where you've seen itA comment box that stores whatever you type and renders it back as HTML to every other reader.

  • Time18 min
  • DiagramStored vs Reflected vs DOM-based flows
  • Chapter3 Β· Security

Diagram

diagram
THREE FLAVOURS β€” same result, different path

  STORED (persistent)          worst: hits every visitor
    attacker ──► POST comment ──► database ──► rendered to all users
                                                       β”‚
                                                  script runs

  REFLECTED (in the URL)       needs a click on a crafted link
    attacker ──► sends link ──► victim ──► server echoes ?q= into HTML
                                                       β”‚
                                                  script runs

  DOM-BASED (never reaches server)
    attacker ──► #hash in URL ──► your JS reads location.hash
                                       └─► innerHTML = value
                                                  script runs
diagram
WHAT THE INJECTED SCRIPT GETS β€” the same origin as you

  document.cookie          β†’ session hijack (unless HttpOnly)
  localStorage             β†’ tokens you stored there
  fetch('/api/transfer')   β†’ acts AS the user, cookies attached
  DOM rewrite              β†’ fake login form on the real domain

  There is no partial XSS. Execution means full compromise of the origin.
diagram
ESCAPING IS CONTEXT-DEPENDENT β€” one function is not enough

  <div>HERE</div>              β†’ HTML escape:  & < > " '
  <a href="HERE">              β†’ URL encode + block javascript:
  <div onclick="HERE">         β†’ never put user data here
  <script>var x = "HERE"</script> β†’ JSON encode, still risky
  <style>color: HERE</style>   β†’ CSS escape

How it works

  1. Untrusted data reaches a sink that parses HTML or evaluates code: innerHTML, outerHTML, document.write, eval, dangerouslySetInnerHTML, srcdoc.
  2. The browser cannot tell your markup from the attacker's β€” it's all one document.
  3. Escape on output, by context, not on input. Frameworks (React, Vue, Angular) escape text nodes by default; the holes are the explicit opt-outs.
  4. For genuine rich text, sanitize with an allowlist (DOMPurify), never with a blocklist or a regex.
  5. Add CSP as the second layer so an injection that gets through still can't execute.
js
// βœ— sink
el.innerHTML = comment;

// βœ“ text, never parsed as markup
el.textContent = comment;

// βœ“ rich text that must keep <b>, <i>, links
el.innerHTML = DOMPurify.sanitize(comment);

Trade-offs

DefenceStopsDoesn't stop
Framework auto-escaping95% of casesdangerouslySetInnerHTML, href
DOMPurify allowlistRich-text injectionBugs in your own sink usage
CSP script-srcExecution of injected scriptThe injection itself
HttpOnly cookiesCookie theftRequests made as the user

Interview angle

"React escapes everything. Are you safe from XSS?"

  • No β€” dangerouslySetInnerHTML, href={userInput} with a javascript: URL, and ref DOM writes all bypass it.
  • Server-rendered JSON embedded in a <script> tag is a second common hole.
  • Layer it: escape by context, sanitize rich text with an allowlist, and ship a CSP for the case you missed.

Recap

  • Any path from untrusted input to an HTML/JS sink is XSS.
  • Escape on output and by context; sanitize with an allowlist when markup is required.
  • CSP and HttpOnly limit the damage of the injection you didn't catch.