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.
import DOMPurify from "dompurify";
function SafeContent({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}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>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
- OWASP: Cross-Site Scripting — comprehensive XSS prevention guide
- DOMPurify — the gold standard HTML sanitization library
- React docs: dangerouslySetInnerHTML — React's explanation of why this prop has "dangerous" in the name