← All Tools

How Markdown Renderers Defend Against XSS, and Why CommonMark Exists

Guide · Last verified Aug 26, 2026

Markdown was designed from the start around the principle that "you can mix raw HTML into plain text." Because of that principle, if you build your own Markdown renderer — or run a service that renders untrusted user input as Markdown — you inherit an XSS (cross-site scripting) attack surface by default. This guide covers why the language ended up this way, which specific inputs are dangerous, and how renderers actually defend against them.

1. Markdown was designed to pass HTML straight through

The original spec for Markdown.pl, created by John Gruber in 2004, explicitly states that "block-level HTML tags pass through without conversion." In other words, if you write <script>alert(1)</script> inside a Markdown document, a renderer that faithfully follows the original spec won't escape it — it will output it as-is as HTML. This isn't a bug; it was a deliberate design choice: "if Markdown can't express some formatting, mix in raw HTML." The problem is that in 2004, when this intent was written, "a service that renders Markdown from many mutually unknown users onto a single page" wasn't nearly as common a scenario as it is today.

2. Real attack vectors: it's not just the script tag

Even a renderer that's aware of the raw-HTML pass-through problem and filters out only <script> tags can still be exploited, because Markdown's own syntax offers separate paths to execute script.

Attack vectorExampleMechanism
Image onerror![x](x.jpg "x" onerror=alert(1))Some parsers pass whatever follows the title straight through as an attribute on the <img> tag
javascript: scheme[click](javascript:alert(1))If the href value isn't validated and is output as-is, clicking it runs the script
data: URI + SVG![x](data:image/svg+xml;base64,...)A <script> can be embedded inside an SVG — it looks like an image but actually carries a script
Reference link redefinition[1]: javascript:alert(1)A link target can be quietly defined with a dangerous scheme somewhere away from the visible body text

In other words, a genuinely safe Markdown renderer can't just filter out the literal string <script> — it needs to handle HTML escaping, a URL scheme allowlist (only permitting things like http/https/mailto), and attribute value escaping, all at the same time.

3. CommonMark: an attempt to standardize a fragmented ecosystem

The original Markdown.pl spec left a lot of ambiguity, and as a result the many implementations that sprang up afterward (Python-Markdown, Pandoc, GFM, Redcarpet, and others) each produced different HTML output for the same input. In 2014, Stack Overflow co-founder Jeff Atwood, John MacFarlane, and others created the CommonMark spec to fix this. CommonMark defines exact expected output for every edge case, backed by a standard spec and test suite, with the goal of eliminating behavioral differences between implementations. In 2017, GitHub officially announced GFM (GitHub Flavored Markdown), built on top of CommonMark and adding tables, strikethrough, autolinks, checkboxes, and more. Today, most web Markdown renderers (marked.js and others) explicitly state that they follow CommonMark or GFM.

That said, the CommonMark spec itself inherited the original "pass raw HTML through" design as-is. Following CommonMark doesn't automatically make a renderer XSS-safe — XSS defense is a layer the renderer has to implement separately from spec compliance.

4. Defense strategies: escaping vs. sanitizing

In practice, there are two broad ways to stop Markdown XSS.

Either way, URL scheme validation (checking that href/src values start with http/https/mailto/tel) is a separate step that's mandatory regardless. Even if HTML tags are perfectly filtered, a single surviving javascript: scheme link is enough for the attack to succeed.

Frequently Asked Questions

Q. Is content automatically safe if it follows CommonMark?

No. CommonMark only guarantees "the same input produces the same output" — it doesn't define a security layer. Passing raw HTML through is part of the spec itself, so XSS defense is something a renderer must implement on top of, separately from spec compliance.

Q. Is it enough to just filter out the script tag?

No, that's not enough. There are several paths to executing code without a <script> tag at all — javascript: scheme links, image onerror attributes, SVG scripts disguised as data: URIs — so filtering a single tag can't close them all off.

Q. Is it risky even if I'm just pasting Markdown to preview it myself?

If you're only writing and viewing your own content, the risk is low. The risk grows in setups where a third party renders Markdown written by someone else (comments, forums, collaborative docs, etc). If you're building a service like that, you need a proven sanitizer like DOMPurify in the pipeline.