VNX-NODE-005: innerHTML or dangerouslySetInnerHTML usage
Reports HTML written into the DOM of a JavaScript or TypeScript file from a non-constant value (insertAdjacentHTML, document.write, React dangerouslySetInnerHTML) that is not sanitized. innerHTML/outerHTML assignments are reported by VNX-NODE-027.
| Field | Value |
|---|---|
| Rule ID | VNX-NODE-005 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | JavaScript / TypeScript |
| CWE | CWE-79 |
| CAPEC | CAPEC-86 |
| ATT&CK | T1059.007 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | xss web react |
| Source | internal/sast/rules/vnx-node-005.rego |
Overview
This rule flags insertAdjacentHTML(), document.write()/writeln() and React’s dangerouslySetInnerHTML prop in JavaScript and TypeScript source files. An assignment to .innerHTML or .outerHTML is the same weakness and is reported by VNX-NODE-027, so one sink is never shown twice; the same sinks in HTML pages and Vue or Svelte components are VNX-NODE-012’s. These 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).
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
// FLAGGED: template literal / concatenation
container.insertAdjacentHTML('beforeend', `<div>${message}</div>`);
document.write("<a href='" + ref + "'>back</a>");
// FLAGGED: one part encoded, the raw value spliced into another
document.write('<a href="/s?q=' + encodeURIComponent(q) + '">' + q + '</a>');
// FLAGGED: React prop, also when split over lines
<div dangerouslySetInnerHTML={{ __html: body }} />
// NOT FLAGGED here: reported by VNX-NODE-027
el.innerHTML = `<h2>${user.name}</h2>`;
node["innerHTML"] = msg;
Not flagged: string literals without interpolation, null, number and boolean literals, ALL_CAPS constants (SPINNER_SVG), values passed whole through DOMPurify.sanitize, sanitize-html or an escape/encode helper (unescape() decodes and is not one), textContent/innerText, comments and strings, minified and vendored files (vendor/, libs/, plugins/, jquery*.js).
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/dompurify -
Set 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.
References
- CWE-79: Improper Neutralization of Input During Web Page Generation (XSS)
- DOMPurify – npm package and documentation
- OWASP XSS Prevention Cheat Sheet
- OWASP Node.js Security Cheat Sheet
- React – dangerouslySetInnerHTML documentation
- MDN – Element.innerHTML security considerations
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-node-005
// nosec vnx-node-005
Or record an ignore rule: vulnetix ignore add --rule vnx-node-005 --reason "...". See Suppressing findings.