Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 6 min read

Markdown Preview in the Browser: What the Sanitiser Drops Before You Copy

Most "markdown preview" boxes on the web do one of two things: they render your Markdown and then quietly fetch every image and follow every link, or they escape the raw HTML and hand you a wall of < characters. OnePa

Most "markdown preview" boxes on the web do one of two things: they render your Markdown and then quietly fetch every image and follow every link, or they escape the raw HTML and hand you a wall of < characters. OnePassApps' Text Studio takes a third route: it renders the Markdown, strips the dangerous parts, and shows you exactly what is left before you copy it.

I wanted to know what survives that strip, so I ran the tab against a file containing a heading, emphasis, a table, a fenced code block, a link, an image, a <script> tag and an inline <b onclick=...>.

Disclosure: I work on OnePassApps.

Open the Text Studio to try it, or read the app guides for the rest of the tools.

The tab, and its smaller input limit

Text Studio has eight tools in one tab strip: Clean, Case, Duplicate lines, Sort lines, Find & replace, Diff, Markdown and Base64. The Markdown tab is the eighth.

The page line over the strip reads "Count, clean, compare and convert text locally. Your content stays in this browser tab." The file picker accepts ".txt, .md, .csv and other text up to 8 MiB ยท or paste it in Input text ยท nothing is uploaded", and the page repeats "Nothing leaves this tab." and "Processing stays in this browser."

One detail worth knowing before you paste: the input counter on the Markdown tab reads 250,000, not 2,000,000. The Clean tab's counter reads 2,000,000. So the Markdown tab has a lower ceiling than its neighbours, and it is the tab where a ceiling matters, because a rendered preview is a heavier operation than a line-ending fix.

The panel says what it does before you press anything

  • 01 Input text โ€” the counter, plus a live Words / Characters / Sentences / Lines / UTF-8 bytes row.
  • 02 Preview โ€” "Markdown renders in this tab without opening links or loading images and scripts. Copy the sanitised HTML when it looks right."
  • 03 Result โ€” "The preview is sanitised: links, images and scripts are never loaded."

That is the contract, and it is worth reading carefully because the middle sentence is narrower than it looks. "Links" are not blocked. Links are stripped of their targets. That is a different thing, and the difference shows up in the output.

The action button reads "Preview Markdown". After a run it becomes "Preview again", and the result panel reports "Ready" and "Rendered in" a fraction of a second, with a "Copy HTML" button and a line under the heading: "Safe preview ยท links, images and scripts are not loaded".

The input I used

Nothing exotic โ€” one heading, one paragraph, a list, a table, a code fence, a link, an image, a script tag and an inline HTML tag with an event handler:

# Heading one

A paragraph with **bold**, *italic*, `inline code` and a [link](https://example.com).

- first item
- second item

| name | role |
| --- | --- |
| Ada | engineer |

```js
const x = 1 < 2 && 3 > 2;
```

<script>alert('xss')</script>

<b onclick="alert(1)">raw html</b>

![alt text](https://example.com/a.png)

The counter read 322 characters, 48 words, 20 lines and 322 UTF-8 bytes.

What came out

Here is the markup that "Copy HTML" put on the clipboard, 427 characters, exactly as it was written:

<h1>Heading one</h1>
<p>A paragraph with <strong>bold</strong>, <em>italic</em>, <code>inline code</code> and a <a>link</a>.</p>
<ul>
<li>first item</li>
<li>second item</li>
</ul>
<table>
<thead>
<tr>
<th>name</th>
<th>role</th>
</tr>
</thead>
<tbody><tr>
<td>Ada</td>
<td>engineer</td>
</tr>
</tbody></table>
<pre><code class="language-js">const x = 1 &lt; 2 &amp;&amp; 3 &gt; 2;
</code></pre>
<p><b>raw html</b></p>
<p></p>

The button confirms it with "Sanitised HTML copied to the clipboard." and the visible preview is the same markup, rendered in the page under the label "Safe preview ยท links, images and scripts are not loaded".

Read that block line by line, because each line is a different rule.

Headings, emphasis, lists, tables and code fences all survive, intact. The table is real table markup, with a thead row and a tbody row. The fenced code block keeps its language class โ€” the opening tag carries the class "language-js" โ€” and the code inside is HTML-escaped the way it should be, so 1 < 2 && 3 > 2 comes out as 1 &lt; 2 &amp;&amp; 3 &gt; 2 instead of being parsed as markup. That escaping is why the code block is safe to paste into a page.

The script tag is gone. Not escaped, not shown as text โ€” gone. There is no script element in the output and no trace of the alert call. The one line it occupied is simply absent.

The link keeps its text and loses its target. The anchor element is still there, still wrapping the word "link" โ€” but it has no href attribute at all. It is not a link to https://example.com any more; it is a piece of text that used to be a link. That is what "links are not loaded" means in practice, and it is the difference between a preview that is safe to paste and one that quietly carries live URLs out of the page.

The image disappears entirely. The markdown image produced no image element and no alt text โ€” just an empty paragraph. So there is nothing to load, and the stripped line leaves a blank paragraph behind it. Worth knowing if you diff the result against your source: an image line becomes an empty paragraph, not a removed line.

Raw HTML is kept but stripped of its attributes. The inline bold tag came through as a bold element with the text "raw html" in it, and the onclick handler was dropped. So the sanitiser does not escape all HTML โ€” it filters it. Tags with dangerous attributes lose those attributes; the safe parts of the tag survive.

What this means for pasting the output

The copy is a fragment, not a full document โ€” no <html>, no <head>, no body wrapper. It starts at the first heading. That is what you want if you are pasting into a CMS, a README, an email composer or a comment box: markup, with the executable parts removed and the loadable parts removed.

It is also not a round trip. Markdown in does not mean Markdown out โ€” what you get is HTML, and the image is the clearest example of a source line that has no counterpart in the result.

What it costs

The plan block on the page reads: "FREE PROMOTIONAL PERIOD", "Standard is free right now", "$0 / month", "100,000 credits every 30 days ยท No end date set ยท No card required" and "No automatic paid subscription. Existing paid subscriptions keep their terms."

Who this is for

If you write Markdown in a tool and paste it into something else, the usual worry is that the paste carries a script, a tracking pixel, or a link that fires a request when someone opens it. This tab answers that worry by removing the targets rather than trusting them, and by showing you the exact output before you copy it โ€” the copy button and the preview are the same bytes.

It does not replace a linter or a sanitiser for content you did not write, and it does not render anything to a file: the preview lives in the tab, and "Copy HTML" is the hand-off.

The rest of the Text Studio sits next to it: Clean, Case, Duplicate lines, Sort lines, Find & replace, Diff and Base64.

Disclosure again: I work on OnePassApps, the team behind app.onepassapps.com.

๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.