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.
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 runsWHAT 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.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 escapeHow it works
- Untrusted data reaches a sink that parses HTML or evaluates code:
innerHTML,outerHTML,document.write,eval,dangerouslySetInnerHTML,srcdoc. - The browser cannot tell your markup from the attacker's β it's all one document.
- Escape on output, by context, not on input. Frameworks (React, Vue, Angular) escape text nodes by default; the holes are the explicit opt-outs.
- For genuine rich text, sanitize with an allowlist (DOMPurify), never with a blocklist or a regex.
- Add CSP as the second layer so an injection that gets through still can't execute.
// β 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
| Defence | Stops | Doesn't stop |
|---|---|---|
| Framework auto-escaping | 95% of cases | dangerouslySetInnerHTML, href |
| DOMPurify allowlist | Rich-text injection | Bugs in your own sink usage |
CSP script-src | Execution of injected script | The injection itself |
HttpOnly cookies | Cookie theft | Requests made as the user |
Interview angle
"React escapes everything. Are you safe from XSS?"
- No β
dangerouslySetInnerHTML,href={userInput}with ajavascript:URL, andrefDOM 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
HttpOnlylimit the damage of the injection you didn't catch.