Keep It Clean
Output encoding (Chapter 6) handles the common case — data shown as text. But sometimes the value is meant to be HTML: a rich-text comment, a CMS article, a formatted message. You can't encode it (that would show the tags as text) and you can't trust it (it may contain a payload). The answer is HTML sanitization: parse the HTML and keep only a known-safe allowlist. This chapter also pins down where input validation genuinely helps — and where it's mistaken for an XSS defence.
When You Actually Need Sanitization
Sanitization is the exception, not the default. You need it only when all three are true: the input is user-provided, it must render as HTML (not text), and you therefore can't simply encode it. If any is false, prefer encoding (Chapter 6) — or avoid raw HTML entirely by accepting Markdown and rendering it through a safe renderer.
Allowlist, Never Blocklist
A sanitizer works by parsing the input into a DOM and keeping only explicitly permitted elements and attributes, dropping everything else. This is the positive model from Chapter 4 — define what's safe, discard the rest — the opposite of trying to strip "bad" tags:
| Approach | Rule | Outcome |
|---|---|---|
| Allowlist (correct) | keep only b, i, p, a[href], ul, li… | unknown/dangerous tags & attrs dropped by default |
| Blocklist (broken) | remove <script>, onerror… | endless bypasses (Chapter 4) — fails |
The Tool: DOMPurify
Don't write an HTML sanitizer — sanitizing HTML correctly is brutally hard (browser parsing quirks, mutation, mXSS), and hand-rolled ones are reliably bypassed. The industry-standard library is DOMPurify:
<svg>/<math>). DOMPurify is maintained specifically to track these and patch them. A hand-rolled regex "sanitizer" will be bypassed; an outdated library may miss new mXSS classes. Rules: use DOMPurify (or your platform's equivalent), keep it current, sanitize with the narrowest allowlist your feature needs, and sanitize as close to output as possible.
Server-Side vs Client-Side Sanitization
DOMPurify runs in the browser and on the server (via jsdom). Where you sanitize depends on where the HTML is inserted:
- DOM-based / SPA — sanitize in the client right before
innerHTML(the DOM-XSS fix from Chapter 2; encoding can't help there). - Server-rendered — sanitize on the server before storing or emitting the HTML.
- Defence in depth — sanitizing on store and escaping/sanitizing on render is reasonable for high-risk HTML.
Where Input Validation Fits (and Where It Doesn't)
Input validation is good practice — but it is not the XSS defence, and treating it as one is a classic mistake.
| Input validation | Output encoding / sanitization | |
|---|---|---|
| Goal | reject malformed/unexpected data (an email looks like an email) | make data safe for its output context |
| XSS role | secondary — reduces bad data, defence in depth | primary — actually prevents execution |
| Why not sufficient | many fields legitimately allow < > " ' (names, comments, code snippets) | neutralizes the payload regardless of input |
O'Brien <the boss>), a bug report containing code, or a message — these legitimately contain < > " ', so you can't reject those characters without breaking the feature. The payload survives validation and must be neutralized at output. Validate for data quality; encode/sanitize for safety. Never substitute the former for the latter.
The Decision Flow
- Does the value need to render as HTML? If no → encode (Chapter 6). Done.
- If yes → can you use Markdown/a structured format instead of raw HTML? If yes, prefer that (rendered safely).
- If you must accept HTML → sanitize with DOMPurify, narrowest allowlist, kept updated.
- Independently → validate structured fields for data quality, and layer CSP (Chapter 8) underneath everything.
Hands-On Exercises
Use DOMPurify to sanitize a rich-text comment that should allow <b>, <i>, and <a href> but nothing else. Show what happens to a payload like <b>hi</b><img src=x onerror=alert(1)>, and explain why allowlisting (not blocklisting) is what makes it safe.
Explain why input validation cannot be the primary XSS defence, using three free-text fields that legitimately contain HTML-special characters. Then give two fields where allowlist validation genuinely does help, and say why the difference is the field's format.
📄 View solutionWalk the decision flow for three features: (a) a username shown on a profile; (b) a blog post body with rich formatting; (c) an account-balance number. State whether each needs encoding, sanitization, or validation (or a mix), and justify by whether the value is text, markup, or a strict format.
📄 View solutionChapter 7 Quick Reference
- Sanitization = parse HTML, keep an allowlist of safe tags/attrs, drop the rest — for when the value must render as HTML
- It's the exception, not the default — encode (Ch. 6) unless you genuinely need to render user HTML
- Prefer a restricted format you control (Markdown rendered safely) over accepting raw HTML when possible
- Allowlist, never blocklist — define what's safe; stripping "bad" tags fails (Chapter 4)
- Use DOMPurify (server via jsdom or client) — never hand-roll; keep it updated for mutation XSS (mXSS)
- Sanitize where HTML is inserted: client before
innerHTML(DOM XSS), or server before store/emit - Input validation is NOT the XSS defence — free-text fields legitimately contain
< > " '; it's data-quality + defence in depth - Allowlist validation helps for strict formats (date, UUID, number, enum, email) — reject anything off-format
- Next chapter: Content Security Policy (CSP) — a browser-enforced safety net layered under encoding/sanitization