Skip to content
Raw HTML and Safe Rendering
step 1/5

Reading — step 1 of 5

Learn

~3 min readProduction Concerns

Raw HTML and Safe Rendering

The escaping lesson ended on an uncomfortable note: escaping is not enough, because CommonMark lets raw HTML through on purpose. That is not a loophole or a bug in some library — it is a conformance requirement, and it is the construct every Markdown security advisory is really about. Time to look at it directly.

What the spec actually requires

Two separate rules produce raw HTML, and a parser needs both.

An HTML block is a run of lines that the block parser hands to the output untouched. Seven kinds exist, each defined by a start line and a stop line:

#Starts withEnds at
1<pre <script <style <textareathe matching close tag
2<!--a line containing -->
3<?a line containing ?>
4<! then a lettera line containing >
5<![CDATA[a line containing ]]>
6< or </ then a known block tag namea blank line
7any other complete tag alone on its linea blank line

Inline raw HTML is the second rule: inside a paragraph, any substring shaped like a tag is emitted verbatim rather than escaped. So a <b>word</b> keeps its tags while 5 < 6 does not, and the difference is decided entirely by whether the text matches the tag grammar.

What does pass-through actually look like?

Markdown inside an HTML block is not parsed — the block is opaque, which is its whole point:

python

The first output is what a conforming renderer must produce. The onclick attribute reaches the browser, and no amount of escaping elsewhere in the parser changes that, because escaping is not applied here by design.

So how do you render untrusted Markdown?

Three answers exist, and only the middle one is complete.

  • Turn raw HTML off. Most libraries have a flag; the reference C implementation replaces each raw block with a comment, which is where the <!-- raw HTML omitted --> line above comes from. Cheap, and it changes what conforming means, so say so in your docs.
  • Sanitize the OUTPUT html with an allow-list of elements and attributes. This is the only approach that also catches a javascript: link destination, which no raw-HTML switch touches.
  • Filter specific tags, the GFM approach: pass everything through except a short list of tag names, whose leading < is escaped. Narrow by construction — anything not on the list still goes straight to the browser.

What does not work is escaping the source before parsing. That turns every author's &copy; into visible noise, breaks link destinations, and still leaves javascript: URLs alone. Escaping belongs at render time, per output position.

Your exercise

Implement the third policy: pass tags through, escape the ones on a list.

The mistake the grader catches is escaping the whole disallowed tag. A visible test feeds A <script>alert(1)</script> attack. and expects A &lt;script>alert(1)&lt;/script> attack. — only the leading < changes, and the > stays a >. Escaping the whole match gives you &lt;script&gt; and a diff on both tags.

The second is the tag name. A hidden test uses <IFRAME>, so compare lowercased, and it also uses <I>, which is not on the list and must survive untouched. And a < that is not part of a tag at all is ordinary text: <3 and <> both become escaped text, not markup.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…

Raw HTML and Safe Rendering — Build a Markdown to HTML Converter