How Markdown Renderers Defend Against XSS, and Why CommonMark Exists
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 vector | Example | Mechanism |
|---|---|---|
| Image onerror | ) | 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 |  | 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.
- Escape all raw HTML up front: convert every
<,>, and&in the input into HTML entities in the very first processing step, so no matter what HTML tag a user types, it only ever displays as text. This is simple and reliably safe, but it gives up one of Markdown's actual features — the ability to embed raw HTML. - Sanitize after rendering: render the Markdown into HTML normally first, then walk the resulting DOM with a library like DOMPurify and strip out any tag, attribute, or URL scheme that isn't on an allowlist. This is more flexible — safe HTML like
<b>or<img>survives, while dangerous elements like<script>oronerrorget removed — but a poorly designed allowlist can leave bypasses open.
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.