VNX-NODE-005 – innerHTML or dangerouslySetInnerHTML Usage
Overview
This rule flags .innerHTML = assignments and uses of React’s dangerouslySetInnerHTML prop in JavaScript and TypeScript source files. Both mechanisms inject raw HTML directly into the DOM. When the injected content includes any user-controlled data — from a database record, URL parameter, API response, or user profile field — an attacker can embed <script> tags or event handler attributes that execute arbitrary JavaScript in other users’ browsers. This is CWE-79 (Improper Neutralization of Input During Web Page Generation — Cross-site Scripting).
Severity: Medium | CWE: CWE-79 – Improper Neutralization of Input During Web Page Generation (XSS)
Why This Matters
Stored XSS via innerHTML is one of the most common and impactful web vulnerabilities. An attacker who finds a stored-XSS path can inject a payload that silently executes every time another user loads the page. Typical payloads steal session cookies, exfiltrate CSRF tokens, hijack form submissions to forward credentials to an attacker-controlled server, or inject a keylogger. In admin panels, a single stored-XSS finding can lead to account takeover for every administrator who views the compromised content.
React specifically named its prop dangerouslySetInnerHTML to make developers pause — but the warning is easy to ignore under deadline pressure, and the property is frequently used to render rich text from a CMS or user-generated HTML. The danger is identical to vanilla innerHTML: if the content has not been sanitised through a dedicated HTML sanitisation library, the application is vulnerable.
What Gets Flagged
The rule matches dangerouslySetInnerHTML (any occurrence) and .innerHTML = assignments in JS/TS/JSX/TSX files.
// FLAGGED: dangerouslySetInnerHTML with dynamic content
function Comment({ text }) {
return <div dangerouslySetInnerHTML={{ __html: text }} />;
}
// FLAGGED: innerHTML assignment
document.getElementById('preview').innerHTML = userInput;
Remediation
Use
textContentinstead ofinnerHTMLwhen you only need to display plain text.textContentsets the text node value and never parses HTML, completely eliminating the XSS vector:// SAFE: textContent does not parse HTML document.getElementById('preview').textContent = userInput;If you must render rich HTML, sanitize it with DOMPurify before assignment. DOMPurify parses the HTML in a sandboxed environment and strips all dangerous elements and attributes:
// SAFE: DOMPurify sanitizes before innerHTML assignment import DOMPurify from 'dompurify'; document.getElementById('preview').innerHTML = DOMPurify.sanitize(userInput);In React, sanitize before passing to
dangerouslySetInnerHTML:// SAFE: sanitize rich text before rendering import DOMPurify from 'dompurify'; function Comment({ text }) { const clean = DOMPurify.sanitize(text); return <div dangerouslySetInnerHTML={{ __html: clean }} />; }Use React’s default JSX rendering for user-supplied text. React’s JSX template syntax (
{variable}) escapes HTML entities automatically — use it wherever you only need to display text, not render markup:// SAFE: JSX escapes HTML entities by default function Comment({ text }) { return <div>{text}</div>; }Install and configure DOMPurify:
npm install dompurify # For TypeScript: npm install --save-dev @types/dompurifySet a Content Security Policy that restricts inline scripts (
script-src 'self') as a defence-in-depth measure. A strong CSP prevents injected<script>tags from executing even if sanitisation is bypassed or absent.