| name | markdown-sanitization-chain |
| description | Render user-supplied markdown safely — marked.js → DOMPurify → Mermaid (order matters; skipping the sanitizer is XSS) |
Markdown Sanitization Chain
Battle-tested via production XSS incident. The order of markdown → sanitize → diagram render is non-negotiable when content comes from users.
When to Use
- An app renders markdown supplied by users (comments, docs UI, embedded editors)
- You're about to call
innerHTML with markdown-derived HTML
- Mermaid or another diagram renderer runs in the browser
- Security review on a markdown-rendering surface
Why It Matters
Markdown renderers (marked.js, markdown-it) convert markdown to HTML but do not sanitize it. Diagram renderers (Mermaid, PlantUML) execute after sanitizers run, which can re-introduce attack vectors. Order matters critically.
Shared Knowledge Gate
Before designing or reviewing a non-trivial markdown-rendering security boundary,
consult scout-knowledge-base for relevant
prior failure modes, procedures, decisions, and gotchas. Read only records
whose signals or preconditions match the boundary, and treat them as context
rather than authority. If unavailable, state that once and continue with the
current threat model; do not create a local fallback while hardening.
The Rule
Always: marked.js → DOMPurify → Mermaid (post-render).
1. Parse markdown to HTML (marked.js)
2. Sanitize HTML (DOMPurify)
3. Insert sanitized HTML into DOM
4. Render diagrams on the now-sanitized DOM (Mermaid.run())
Never skip the sanitizer even if content is "trusted." Trust gets revoked when the threat model changes; the chain stays.
Implementation
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import mermaid from 'mermaid';
async function renderMarkdown(content, container) {
const rawHtml = marked.parse(content);
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ADD_TAGS: ['mermaid'],
});
container.innerHTML = cleanHtml;
await mermaid.run({ nodes: container.querySelectorAll('.mermaid') });
}
Common Mistakes
| Mistake | Consequence |
|---|
| Skip DOMPurify ("it's internal content") | XSS from any content source |
| Sanitize after Mermaid renders | Mermaid-injected scripts execute |
Use innerHTML without sanitization anywhere | Classic XSS |
| Trust localStorage / URL params | User-controlled XSS payloads |
DOMPurify Configuration
const config = {
ADD_TAGS: ['mermaid'],
ADD_ATTR: ['class'],
FORBID_TAGS: ['style', 'script'],
FORBID_ATTR: ['onerror', 'onload', 'onclick'],
};
Do not allow on* event attributes by default. If a product genuinely needs an event handler, bind it in trusted application code after sanitization instead of allowing user-supplied event attributes through DOMPurify.
Verification Checklist
Related
Would Revise If
Revisit this skill by 2026-11-30 or sooner if any of the following fires: DOMPurify or marked.js publishes a breaking change that invalidates the documented chain order; a real XSS payload bypasses the chain in production use; or Mermaid changes its render-time HTML interface in a way that makes the post-sanitization step unsafe.