chibi@unraed: blog / 2020-08-19-11:12 $

Krammit! A close look at the recent kramdown vulnerability

Posted: 2020-08-19 11:12
Tags: , ,

Image: krammit

Skimming the latest security updates for Linux distributions the other day, my eyebrow shot up when I saw that ruby-kramdown was one of the packages urgently requiring an update due to a vulnerability. Kramdown (project homepage) is a Markdown parser and converter. For example, I use it on this website to convert blog posts written in Markdown into the HTML which your browser can display to you.

Initially, I could not guess what the urgent security issue could be. Kramdown’s activities seem to be limited to parsing and converting purely stylistic elements, such as paragraphs, lists and tables. What kind of Markdown-formatted text could an attacker actually put into kramdown that would give an interesting result?

The main lesson of this vulnerability is that Kramdown does more than you think. In fact, it does enough that an attacker who controls the Markdown input to the program can basically run any Ruby code he likes. Any server which uses kramdown to process user-generated Markdown was potentially vulnerable. Apparently, this included GitHub Pages, theoretically making GitHub’s servers vulnerable to an attacker’s Ruby code (before the security update, at least). Ouch.

In this post, I’ll look at the vulnerability itself, possible ways of exploiting it and a general security lesson which can be drawn from this incident. For the avoidance of doubt, nothing in this post implies that I think kramdown is bad software. It’s not - just like all useful software, it is imperfect from a security point of view.

Wham, bam, thank you Kram: the vulnerability

As I said, kramdown parses Markdown and converts it into HTML (as well as other formats). Typically, kramdown does not actually output a full HTML document; rather, it outputs an HTML document fragment, which lacks tags such as <html>...</html> marking the beginning and end a full document. This is useful when the user wants to control these important bits of the final HTML document independently.

One way in which a fragment can be completed with the HTML necessary for a full document is through a kramdown option called template. With this, the user can specify some HTML code in which to wrap the document fragment before kramdown outputs it.

Conveniently, the user can actually specify the template to use from within the Markdown document being processed. This is thanks to an extension made by kramdown to the common Markdown syntax:

{::options template="path/to/template.html" /}

So far, there is nothing particularly worrying. Two more details change the security situation dramatically, however.

First, the HTML in the template can contain Embedded Ruby (ERB) code. Kramdown will run this without question when it puts together the full document.

Second, kramdown also provides a mechanism to write the template code directly in the options block. Specifically, it works like this:

{::options template="string://<template_code>" /}

Anything appearing after string://, including ERB code, gets used as the template. This means that a user does not even have to get permission to store files on the filesystem in order to execute Ruby code embedded in an HTML template on that system.

The vulnerability in a nutshell: anyone who can control the content of a Markdown document can run arbitrary Ruby code on the machine which processes that Markdown.

Season liberally with mixed ERBs

L33T H4XX0RZ already know what’s coming. Yes, there are websites which take Markdown input from users and convert it into HTML. Most notably, GitHub Pages, which lets GitHub users create static websites for free, makes use of Jekyll, which in turn defaults to kramdown for Markdown parsing. Of course, we do not know if GitHub was particularly vulnerable to the vulnerability. There is a raft of sensible mitigations they might have had in place in anticipation of defects in their server-side code.

But… let’s speculate anyway about what an attacker might have done to an unguarded website running kramdown on user input. By submitting Markdown containing the following code, the attacker can…

{::options template="string://<%= File.new('/etc/passwd', mode: 'r').read(nil) %>" /}
{::options template="string://<%= Dir.children('/usr/lib') %>" /}
{::options template="string://<% system('touch heres_johnny.txt') %>" /}

…and so on.

The amount of damage that an attacker can do depends on the restrictions applied to the server-side user which runs kramdown. In the real world, I hope that no server runs kramdown as root. But even if this is not the case, it is likely that the user can read most of the filesystem and modify some files, including, perhaps, public parts of a website. That’s still pretty bad news.

A tale of two design choices

Once someone has made you think about kramdown’s options block, the potential for abuse is obvious. So how did this vulnerability ever make it into the code?

In general terms, this security hole came about because two different usages of kramdown collided. On the one hand, kramdown can be used as a stand-alone program on the command-line; this usage is intended for local users to convert files one-by-one, with direct oversight by the user. Of course it is a reasonable design choice, for this usage, to enable an option for setting a user-supplied template.

On the other hand, kramdown is used to process user-generated Markdown in an automated fashion. Since the user, in this case, is a remote one, and so cannot tweak the appearance of individual documents by specifying options to kramdown on the command-line, it is desirable to make the options available from within the document. The simplest design choice is just to let all command-line options be controlled with a special syntax - and that is what kramdown did.

In each usage, the design choices were rational. But, when they are considered together, they are a security nightmare. Incidentally, this confirms what computer security people often say about their work. Understanding a program’s security is not just understanding how the program’s individual parts work, but also understanding how these parts interact in potentially unexpected ways. And unexpected behaviour can lurk anywhere on your system - even in an innocent-looking Markdown converter.