Skip to content

vibe-xss-dangerously-set

CategoryDefault severityLifecycleDefault confidence
securityBLOCKER (literal: MAJOR)beta0.9 dynamic / 0.75 literal

Pack: core-security

What it catches

React's dangerouslySetInnerHTML={{ __html: <expr> }} attribute, anywhere in TSX / JSX (and .ts / .js files that import React and use JSX). The matcher classifies the captured __html expression:

Expression shapeSeverityWhy
Variable, prop, function call, member accessBLOCKERSource is dynamic — any path that lets a user contribute to the value is stored XSS.
Template literal containing ${…}BLOCKERInterpolation injects values into the rendered HTML.
Plain string literal ('…', "…", untemplated … )MAJORNo immediate XSS, but the markup should be JSX nodes so React's escaping protects you when the value later becomes dynamic.
Pure inline SVG literalMAJORLower-urgency design smell. Prefer an Icon library / imported SVG component.

Why it matters

React deliberately named this attribute "dangerously". Anything passed via __html bypasses React's escaping and is inserted into the DOM as live HTML.

  • Veracode's 2025/26 study found 86% of AI-generated code samples failed to defend against XSS.
  • dangerouslySetInnerHTML is the single React API most often used as the unsafe sink. Prompts like "render this markdown as HTML" or "insert this rich-text response into the page" commonly produce this exact attribute, frequently without a sanitiser.

The same code that ships a one-off "render the LLM's reply" feature is the code that ships persistent stored-XSS if the reply path is ever attacker-influenced.

Example: failing code

// BLOCKER — prop is dynamic, untrusted by default
<div dangerouslySetInnerHTML={{ __html: body }} />

// BLOCKER — template literal interpolates user content
<footer dangerouslySetInnerHTML={{ __html: `<small>by ${title}</small>` }} />

// MAJOR — string literal markup (design smell, not immediate XSS)
<div dangerouslySetInnerHTML={{ __html: '<em>End of post</em>' }} />

Example: how to fix

(a) Prefer JSX — works for most cases

<div>{body}</div>

React escapes body automatically. If you need bold / italic structure, render it as JSX nodes:

<div><em>End of post</em></div>

(b) If the source is rich text from a trusted pipeline, sanitise first

import DOMPurify from 'dompurify';

<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />

DOMPurify (or sanitize-html, or similar) strips script tags, event handlers, and other XSS vectors. The sanitiser must wrap the `__html` value directly — sanitising once at the source and then transforming further loses the protection.

(c) If you're rendering Markdown, use a Markdown component

import ReactMarkdown from 'react-markdown';

<ReactMarkdown>{markdownSource}</ReactMarkdown>

Known limitations

  • The matcher is regex-based. Deeply-nested attribute syntax (e.g. JSX inside a complex object spread) may slip past. A JSX-AST path lands when more frontend rules need it.
  • JSX-as-string-content matches too. If you embed a string literal that contains the JSX attribute as text (e.g. a docs page showing the attribute as an example), the rule will fire. Suppress those lines with // codemore-ignore-next-line: vibe-xss-dangerously-set. Removing this limitation requires AST-aware string-literal stripping.
  • Values containing literal `}}` terminate early. Extremely rare in real JSX attributes; the rule would simply not match those.
  • We do not detect whether the expression is already sanitised (DOMPurify.sanitize(x)). A future v1.1 will inspect the AST and downgrade severity when a recognised sanitiser wraps the source.
  • We do not look at imports to confirm dangerouslySetInnerHTML is the React attribute — but the JSX syntax <elem dangerouslySetInnerHTML={…} /> is React-specific and the pattern is unambiguous in practice.

Suppression

If the source is a trusted compile-time constant (e.g. a tiny inline SVG from a vetted source):

{/* codemore-ignore-next-line: vibe-xss-dangerously-set */}
<span dangerouslySetInnerHTML={{ __html: TRUSTED_SVG_LITERAL }} />

Per file:

/* codemore-ignore-file: vibe-xss-dangerously-set */

Or disable globally in .codemorerc.json:

{ "rules": { "vibe-xss-dangerously-set": "off" } }

References

Next →
Back to the catalog
See the other 63 rules — grouped by pack, with lifecycle gates.