VNX-NODE-027 – Assignment to innerHTML without sanitization
Overview
This rule detects direct assignment to the .innerHTML property in JavaScript files. Setting innerHTML to a value that incorporates user-controlled data causes the browser to parse the string as HTML, executing any embedded script tags, event handlers (onerror, onload, etc.), or JavaScript URLs (javascript:). This is classified as CWE-79 (Improper Neutralization of Input During Web Page Generation) and is one of the most common sources of DOM-based XSS in modern web applications.
Unlike reflected or stored XSS that originates on the server, DOM-based XSS from innerHTML assignment occurs entirely in the browser. The payload never passes through the server, making it invisible to server-side input validation and many web application firewalls. The vulnerability is triggered when client-side JavaScript reads attacker-controlled data — from the URL fragment, location.search, document.referrer, postMessage events, or local storage — and writes it into the DOM via innerHTML.
Severity: High | CWE: CWE-79 – Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’) | OWASP: A03:2021 – Injection | CAPEC: CAPEC-63 – Cross-Site Scripting (XSS) | ATT&CK: T1059.007
Why This Matters
DOM XSS is actively exploited in the wild and is frequently discovered in widely-used JavaScript libraries and single-page applications. An attacker who achieves script execution in a victim’s browser can steal session cookies, capture keystrokes, redirect to phishing pages, make authenticated API requests, or exfiltrate data from the page. Modern browsers have removed synchronous <script> execution via innerHTML but remain vulnerable to event handler injection (<img onerror="...">), which is sufficient for full script execution.
In Node.js SSR (server-side rendering) contexts — for example, React/Vue/Angular applications with Express back-ends that pass props via dangerouslySetInnerHTML or equivalent — the same sink can produce reflected XSS that is indexed by search engines and easily shared as a malicious link.
What Gets Flagged
// FLAGGED: user input from URL assigned directly to innerHTML
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
// FLAGGED: server-provided data injected into DOM without sanitization
element.innerHTML = apiResponse.htmlContent;
// FLAGGED: template string with user data
container.innerHTML = `<p>${req.body.comment}</p>`;
Remediation
- Use
textContentinstead ofinnerHTMLwhen inserting plain text — it is never interpreted as HTML and requires no sanitization. - When HTML structure must be inserted dynamically, sanitize the input with DOMPurify before assigning it to
innerHTML. Configure DOMPurify with a strict allowlist appropriate to your use case. - Use DOM construction APIs (
document.createElement,element.appendChild,element.setAttribute) to build HTML programmatically rather than via string parsing. - In frameworks like React, avoid
dangerouslySetInnerHTML; if unavoidable, pass the value through DOMPurify first.
// SAFE: textContent for plain text — never parsed as HTML
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').textContent = 'Hello, ' + name;
// SAFE: DOMPurify sanitization before innerHTML assignment
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(apiResponse.htmlContent, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href'],
});
element.innerHTML = clean;
// SAFE: DOM API construction — no HTML parsing
const p = document.createElement('p');
p.textContent = userComment;
container.appendChild(p);