Markdown raw HTML: allow, escape, or sanitize?
Decide whether your Markdown renderer should pass raw HTML through, escape it, or use an allowlist for trusted authors.
10 Aug 2025, 14:35 UTC

The decision and constraints
Your Markdown renderer receives a comment that contains `<img src=x onerror=alert(1)>`. If raw HTML passes through unchanged, that markup can execute JavaScript in your users' browsers. If the renderer escapes every tag, authors lose embedded diagrams, callout boxes, and legacy HTML tables. The practical default is to disable raw HTML for untrusted input and use an allowlist only for trusted authors who need embedded markup.
Make the decision based on three constraints:
- Input trust. Is the Markdown written by internal maintainers or by anonymous users?
- Required markup. Do authors need iframes, CSS classes, or HTML tables that Markdown cannot express?
- Markdown flavor. CommonMark and GitHub Flavored Markdown (GFM) handle raw HTML differently, and renderer defaults vary by library.
Supported rendering options compared
| Option | What happens to raw HTML | Best suited for | Primary risk |
|---|---|---|---|
| Allow raw HTML | HTML blocks and inline tags pass through to the output unchanged. | Trusted internal documentation where authors need full control. | Cross-site scripting (XSS) if untrusted input reaches the renderer. |
| Escape all HTML | Characters such as < and > are encoded to < and >, so markup appears as text. | User comments, forums, and any untrusted input. | Breaks embeds, inline <div> wrappers, CSS classes, and legacy HTML tables. |
| Allowlist sanitization | A DOM-based sanitizer permits a limited set of tags and attributes while removing or neutralizing everything else. | Trusted authors who need a few safe HTML features without full HTML access. | Configuration errors or parser bypasses if the allowlist is incomplete or regex-based. |
Trade-offs by input source
Allowing raw HTML gives authors maximum flexibility but shifts the security burden to every downstream consumer. It is acceptable only for trusted writers and should be paired with a Content-Security-Policy header as defense in depth.
Escaping all HTML is the safest choice for untrusted input, but authors will see literal angle brackets instead of formatted widgets. Existing documentation that depends on inline <div> elements or CSS class attributes may break when this mode is enabled.
Allowlist sanitization is the middle ground. It permits safe tags such as <a> and <br> while rejecting <script>, <iframe>, and event-handler attributes like onerror. Use a maintained DOM parser and sanitizer; regex-based HTML filtering is fragile and can be bypassed by malformed or nested markup.
Concrete implementation sketch
Option names vary by library, so check your renderer's documentation for settings such as `html`, `safeMode`, `escapeHtml`, or `sanitize`. The following is a configuration sketch, not a tested snippet:
# Pseudocode; adapt option names to your Markdown library.
renderer = new MarkdownRenderer({
html: false, # escape or strip raw HTML blocks and inline HTML
})
# For trusted authors, pass rendered HTML through a DOM-based sanitizer.
safeHtml = sanitizer.clean(renderedHtml, {
allowedTags: ['a', 'b', 'i', 'br'],
allowedAttributes: { a: ['href'] },
})
Do not rely on regex-based filtering as the sole defense. A sanitizer that parses HTML into a DOM tree is less likely to miss malformed or nested markup.
Validation example
Render this probe document:
Check <a href='https://example.com'>this link</a> and <script>alert(1)</script>.
Expected behavior with raw HTML disabled:
Check <a href='https://example.com'>this link</a> and <script>alert(1)</script>.
Expected behavior with an allowlist that permits <a>:
<a href='https://example.com'>this link</a> and .
In both cases, the script element must not appear as an executable tag in the output HTML.
Validation steps
- Render a probe document containing <script>alert(1)</script> and an allowlisted tag such as <br>.
- Inspect the serialized HTML: the script tag should appear escaped or removed, never as an executable script element.
- Check renderer configuration for the raw HTML toggle and confirm the default setting before changing it.
- Compare the same probe across CommonMark and GFM renderers if your pipeline supports both, and document which tags are stripped versus escaped.
Limitations and how to check the result
Sanitization reduces but does not eliminate injection risk. Combine it with Content-Security-Policy headers so that even if a dangerous attribute slips through, the browser will not execute inline scripts.
Be aware that markup rendering on GitHub may be escaped or stripped in a strict CommonMark implementation. If you migrate documentation between platforms, test the same HTML-containing Markdown in both environments.
Practical check: after rendering, search the output for <script, onerror, onload, and <iframe. None of these should appear in executable form for untrusted input.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.