usability.cat
Issue Wiki

Dangerous innerHTML Usage

Letting strangers write on your whiteboard — that's what unsanitized innerHTML does. It opens the door to cross-site scripting attacks.

What is this?

You have a whiteboard in your office. Imagine letting any stranger off the street walk in and write whatever they want on it — marketing slogans, obscene drawings, or instructions that trick your coworkers into handing over the company credit card. That is what innerHTML and React's dangerouslySetInnerHTML do when you use them with unsanitized user input. You are giving external content the power to inject arbitrary HTML and JavaScript into your page, enabling cross-site scripting (XSS) attacks.

Why it matters

  • For your visitors: An XSS attack means an attacker can run code in your visitors' browsers as if it came from your site. They can steal cookies, session tokens, and personal data. They can redirect visitors to phishing pages. They can make actions on behalf of your visitors without their knowledge. Your visitors trust your domain — XSS abuses that trust.
  • For your business: XSS is consistently in the OWASP Top 10 security vulnerabilities. A single XSS exploit can lead to data breaches, account takeovers, legal liability, and destroyed reputation. "We let attackers inject scripts" is not a conversation you want to have with your users.
  • The standard: Never insert untrusted content as raw HTML. Always sanitize HTML before rendering it, or better yet, avoid raw HTML insertion entirely. Use text content methods or a sanitization library like DOMPurify.
Sanitized HTML rendering
import DOMPurify from "dompurify";

function SafeContent({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Raw unsanitized HTML
function UnsafeContent({ html }: { html: string }) {
  // DANGEROUS: renders any HTML including <script> tags
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

How to fix it

React / Next.js

The safest approach is to avoid dangerouslySetInnerHTML entirely. When you must render HTML (like from a CMS or markdown), always sanitize it first.

import DOMPurify from "dompurify";

// Option 1: Sanitize with DOMPurify (best for dynamic content)
function RichContent({ html }: { html: string }) {
  const sanitized = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["p", "strong", "em", "a", "ul", "ol", "li", "h2", "h3", "br"],
    ALLOWED_ATTR: ["href", "target", "rel"],
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

// Option 2: Use textContent for user-generated text (safest)
function UserComment({ text }: { text: string }) {
  // React automatically escapes text content — no XSS possible
  return <p>{text}</p>;
}

// Option 3: Parse markdown instead of accepting raw HTML
import { marked } from "marked";

function MarkdownContent({ markdown }: { markdown: string }) {
  const html = marked.parse(markdown);
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Never do this:

// NEVER pass user input directly to dangerouslySetInnerHTML
const userInput = searchParams.get("comment");
<div dangerouslySetInnerHTML={{ __html: userInput }} />; // XSS vulnerability!

// NEVER use innerHTML in a ref callback
const divRef = useRef<HTMLDivElement>(null);
divRef.current.innerHTML = userProvidedContent; // also XSS!

Plain HTML

If you must use innerHTML in vanilla JavaScript, sanitize first.

<div id="content"></div>

<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<script>
  // SAFE: sanitize before insertion
  const userContent = getContentFromAPI();
  const clean = DOMPurify.sanitize(userContent);
  document.getElementById("content").innerHTML = clean;

  // SAFEST: use textContent for plain text
  const userName = getUserName();
  document.getElementById("username").textContent = userName;
  // textContent cannot execute scripts — it renders everything as text
</script>
High impactsecurity~2 paws

XSS is one of the most common and dangerous web vulnerabilities. The cat takes unsanitized innerHTML very seriously — this is a red flag that can compromise every visitor.

How the cat scores this

The scanner looks for uses of innerHTML, outerHTML, dangerouslySetInnerHTML, and document.write() in your JavaScript. It then checks whether the content being inserted is sanitized (by looking for DOMPurify or similar sanitization patterns). Unsanitized raw HTML insertion is flagged as a high-severity security issue. The scanner also checks for inline event handlers in HTML attributes (onclick, onerror) that accept dynamic values, as these are another XSS vector.

Further reading

On this page