Using Markdown Securely

by James Adarich | 10/03/2021

Markdown logo

Markdown is a markup langauge for text formatting, however under the surface lies a whole host of functionality which could leave your app open to abuse and attack.

What's the big deal?

Injecting XSS into Markdown is very easy, just check out this totally normal link for example.

1 [Click for XSS](javascript:alert('You just got hacked!'))
2

Also, Markdown supports HTML so the following is also completely valid.

1 <script>console.log("here be hacks!")</script>
2

Beyond this there are even more options but I think the point has been made.

Most Markdown libs have a "sanitize" functionality but they acknowledge that these don't work properly / are deprecated

marked sanitize warning

showdown acknowledgement

OK, how do I secure my Markdown then?

Good news is there are loads of options to properly sanitize the HTML output from a Markdown libaray. Below is an example using dompurify inside a react component.

1 import * as marked from "marked";
2 import * as React from "react";
3 import { sanitize } from "dompurify";
4
5 interface MarkdownEditorProps {
6 value: string;
7 }
8
9 export function MarkdownEditor(props: MarkdownEditorProps) {
10 const [markdownValue, setMarkdownValue] = React.useState(""));
11
12 const onChange = React.useCallback((value, valid) => {
13 setMarkdownValue(sanitize(marked(value)));
14 }, []);
15
16 return (<textarea value={markdownValue} onChange={onChange} />;
17 }
18

But what about the back end?

Yep, of course we can't trust anything coming from a client so we'll have to do it on the back end too.

If you're using node you can use dompurify again in exactly the same way but let's make this a bit more difficult with some C#. This is just as simple see the example below using HtmlSanitizer.

1 namespace YourApp.Namespace
2 {
3 using Ganss.XSS;
4
5 public class HtmlSanitizationService
6 {
7 private readonly IHtmlSanitizer htmlSanitizer;
8
9 public HtmlSanitizationService(IHtmlSanitizer htmlSanitizer)
10 {
11 this.htmlSanitizer = htmlSanitizer;
12 }
13
14 public string Sanitize(string unsanitizedHtml)
15 {
16 return this.htmlSanitizer.Sanitize(unsanitizedHtml);
17 }
18 }
19 }
20

Great, now how do I display this safely?

Just use the dompurify sanitize function again and your golden (note regardless of where HTML is coming from you should always sanitize it coming into dangerouslySetInnerHTML. The name of the prop should be a clue!)

1 import * as React from "react";
2 import { sanitize } from "dompurify";
3
4 interface HtmlRendererProps {
5 value: string;
6 }
7
8 export function HtmlRenderer({ value }: HtmlRendererProps) {
9 return (
10 {% raw %}
11 <div
12 dangerouslySetInnerHTML={{
13 __html: sanitize(value)
14 }}
15 {% endraw %}
16 />
17 );
18
19 }
20

Wait! What about when the user wants to edit?

Perfect, you've found the hole in the solution. As we've had to rely on HTML sanitizers to get the job done we've inadvertently changed the data type. So when the user comes to edit a field, suddenly they are presented with some HTML and not the Markdown they'd initially input. Fortunatly, turndown gives us the ability to reverse engineer the Markdown from the HTML so all we need to do is use this when we're loading the value back into our component's state.

1 import * as marked from "marked";
2 import * as React from "react";
3 import { sanitize } from "dompurify";
4 import TurndownService from "turndown";
5
6 interface MarkdownEditorProps {
7 value: string;
8 }
9
10 export function MarkdownEditor(props: MarkdownEditorProps) {
11 const turndownService = new TurndownService();
12 const [markdownValue, setMarkdownValue] = React.useState(
13 turndownService.turndown(props.value)
14 );
15
16 const onChange = React.useCallback((value, valid) => {
17 setMarkdownValue(sanitize(marked(value)));
18 }, []);
19
20 return (<textarea value={markdownValue} onChange={onChange} />;
21 }
22

Awesome, can you just briefly summarize that for me?

For sure;

  1. Sanitize the Markdown in the app
  2. Sanitize the converted HTML before storing
  3. Ensure you sanitize the stored HTML before you display it
  4. Remember to convert the HTML back to Markdown before editing

So now I'm using sanitizers I can relax!

Not quite, sanitizers are far from perfect and it could be that a sanitizer misses an attack vector. Always ensure your dependencies are up to date in order to minimize your exposure to this risk.

As a side note this highlights the importance of implementing a strong Content Security Policy as if we miss a potential XSS attack we can fall back to this to help protect our application and users.


Originally posted on James's blog


Share this article

You Might Also Like

Explore more articles that dive into similar topics. Whether you’re looking for fresh insights or practical advice, we’ve handpicked these just for you.

AI Isn’t Magic: Why Predictive Accuracy Can Be Misleading

by Frans Lytzen | 15/04/2025

One of the biggest misconceptions in AI today is how well it can actually predict things – especially things that are rare. This is most directly applicable to Machine Learning (as they are just statistical models) but the same principle applies to LLMs. The fundamental problem is the same and AI is not magic. In reality, AI’s predictive power is more complicated. One of the key challenges? False positives—incorrect detections that can significantly undermine the value of AI-driven decision-making. Let’s explore why this happens and how businesses can better understand AI’s limitations.

From Figma Slides to Svelte Page in Under an Hour – How I Accidentally Proved My Own Point

by Marcin Prystupa | 10/04/2025

A quick case study on how I went from a Figma presentation to a working Svelte page in less than an hour – with the help of AI and some clever tooling.

Embracing the European Accessibility Act: A NewOrbit Perspective

by George Elkington | 12/03/2025

As the European Accessibility Act (EAA) approaches its enforcement date on June 28, 2025, businesses must prioritise accessibility to ensure compliance and inclusivity. The EAA sets new standards for software, e-commerce, banking, digital devices, and more, aiming to make products and services accessible to all, including people with disabilities and the elderly. Non-compliance could lead to significant penalties across the EU. At NewOrbit, we believe that accessibility is not just a legal requirement—it’s good design. Take advantage of our free initial review to assess your compliance and stay ahead of the deadline.

Contact Us

NewOrbit Ltd.
Hampden House
Chalgrove
OX44 7RW


020 3757 9100

NewOrbit Logo

Copyright © NewOrbit Ltd.