Dynamic DOM injection — document.write(), innerHTML assignments with external content, and dynamically created <script> elements with user-controlled sources — creates XSS vulnerabilities that CSP cannot fully mitigate. An attacker who controls any input to these patterns achieves script execution in the user's browser context: session token theft, credential harvesting, or full account takeover. CWE-79 (XSS) and CWE-83 (Script in HTML Attribute) both apply directly. OWASP A03 (Injection) lists DOM-based XSS as a primary injection vector. These patterns persist in codebases long after their introduction because they appear to work correctly during normal operation — failures only surface under adversarial input.
Critical because `innerHTML` with external content, `document.write()`, and dynamic script injection are direct XSS vectors that enable arbitrary script execution regardless of CSP configuration, since CSP cannot prevent patterns already executing in trusted script context.
Replace DOM injection patterns with safe equivalents. Text content should use textContent; structured content should use the DOM API or framework rendering.
// DANGEROUS: XSS vector
element.innerHTML = apiResponse.html
document.write('<script src=' + userInput + '></script>')
// SAFE: text only
element.textContent = apiResponse.text
// SAFE: structured DOM construction
const p = document.createElement('p')
p.textContent = apiResponse.title
container.appendChild(p)
// SAFE: React auto-escapes
return <div>{apiResponse.content}</div>
// LAST RESORT: sanitized HTML (treat as tech debt)
import DOMPurify from 'dompurify'
element.innerHTML = DOMPurify.sanitize(apiResponse.html)
DOMPurify reduces risk but does not eliminate the injection pattern — flag every innerHTML assignment in code review regardless of sanitization.
ID: security-headers-ii.supply-chain.no-dynamic-injection
Severity: critical
What to look for: Search all JavaScript/TypeScript source files for:
document.write() calls — count all occurrencesinnerHTML or outerHTML assignments with external or user-controlled content — count all occurrences where the assigned value comes from user input, URL parameters, API responses, or any external source<script> elements with user-controlled src attributes — count occurrences of createElement('script') or template literals building script tagsReport: "X document.write calls, Y innerHTML assignments with external content, Z dynamic script injections found."
Pass criteria: All counts are 0 — 0% of source files contain DOM injection patterns. Report the counts even on pass.
Fail criteria: Any count is greater than 0.
Do NOT pass when: innerHTML is used with DOMPurify sanitization — that is a mitigation, not an elimination. This check is specifically about DOM injection patterns, not sanitization quality. DOMPurify reduces risk but does not eliminate the attack surface.
Skip (N/A) when: Never skip — this check applies to every project with JavaScript source files.
Cross-reference: For XSS prevention patterns beyond headers, the Client-Side Framework Security audit covers DOM sanitization.
Detail on fail: "X document.write calls, Y innerHTML assignments with external content, Z dynamic script injections found" or "innerHTML used with API response data in UserProfile component — DOMPurify present but pattern is still flagged"
Remediation: DOM injection patterns create XSS vulnerabilities regardless of sanitization. Replace them with safe alternatives:
// DANGEROUS: innerHTML with external content
element.innerHTML = apiResponse.content
// SAFE: textContent for text-only content
element.textContent = apiResponse.content
// SAFE: DOM API for structured content
const p = document.createElement('p')
p.textContent = apiResponse.title
container.appendChild(p)
// SAFE: React/framework rendering (auto-escapes)
return <div>{apiResponse.content}</div>
If you must render HTML, use your framework's built-in sanitization (React's dangerouslySetInnerHTML with DOMPurify) and treat it as technical debt to eliminate.