# No dynamic script injection with user-controlled content

- **Pattern:** `ab-002428` (`security-headers-ii.supply-chain.no-dynamic-injection`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002428
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002428)

## Why it matters

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.

## Severity rationale

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.

## Remediation

Replace DOM injection patterns with safe equivalents. Text content should use `textContent`; structured content should use the DOM API or framework rendering.

```ts
// 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.

## Detection

- **ID:** `no-dynamic-injection`
- **Severity:** `critical`
- **What to look for:** Search all JavaScript/TypeScript source files for:
  1. `document.write()` calls — count all occurrences
  2. `innerHTML` 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
  3. Dynamically created `<script>` elements with user-controlled `src` attributes — count occurrences of `createElement('script')` or template literals building script tags

  Report: "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:

  ```ts
  // 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.

## External references

- cwe CWE-79
- cwe CWE-83
- owasp A03

Taxons: injection-and-input-trust

HTML version: https://auditbuffet.com/patterns/ab-002428
