20 December 2018

Inlining JSON in a Jekyll Liquid Template

Recently while working with the Liquid templating language in a Jekyll project, I found that I wanted to inline JSON data on a page, so I could render it dynamically with JavaScript. The quickest—but fundamentally insecure—way to do this is by using the jsonify filter to add an object to the page and then access that object in your script (React, Vue, Angular, whatever):

<script>
  window._DATA = {{ page.data | jsonify }}; // this introduces an XSS exploit
</script>

Unfortunately, the jsonify filter is not secure (currently using Jekyll 3.8.5). In the above example, if page.data is: </script><script>alert('hi!')</script>, then you’ll quickly see that this snippet of text gets injected verbatim into your template and you will receive a friendly greeting alert when you load the page. This is a cross-site scripting exploit (XSS).

The secure way that jsonify should work (which I think is how the Shopify json filter works) is to convert < and > within any strings it encounters to their unicode escape sequence, e.g., < -> \u003C and > -> \u003E and & -> \u0026. For additional context, here’s someone looking to handle the same issue while using expressjs.

To get around this issue in Jekyll, you can add an additional escape step to convert the whole object into an escaped string, then decode and parse it afterward:

<script>
  window._DATA = JSON.parse(decodeURIComponent("{{ page.data | jsonify | uri_escape  }}"));
</script>

Now you’re safely preventing JavaScript from sneaking out of your data into the page!

Inline JSON tag

_includes/json.html:

JSON.parse(decodeURIComponent("{{ include.data | jsonify | uri_escape  }}"))

So you can now do: {% include json.html data=foo %}

Inline JSON tag for subset of object

_includes/json_from_keys.html:

{% assign keys = include.keys | split:"," %}{ {% for k in keys %}{{ k | jsonify }}: JSON.parse(decodeURIComponent("{{ include.data[k] | jsonify | uri_escape  }}")){% unless forloop.last %}, {% endunless %}{% endfor %} }

So you can now select only specific keys within the object, as such:
{% include json_from_keys.html data=foo keys="name,body,tags" %}

Happy coding!

Edit: FWIW, I noticed regular variables in templates aren’t escaped by default either. For example in a snippet such as {% assign foo = "This & that > those" %}{{ foo }} the > and & do not get html-escaped. This seems really strange and backward to me—perhaps someone more familiar with Jekyll has an idea why it works this way? I could see someone arguing that the content is all pre-generatd so you don’t have to worry about random UGC sneaking in to create XSS attacks, but basic HTML-escaping of content still seems like an entry-level feature for any modern HTML templating language?




Did you find this helpful or fun? paypal.me/mrcoles
comments powered by Disqus

Peter Coles

Peter Coles

is a software engineer living in NYC who is building Superset 💪 and also created GoFullPage 📸
more »

github · soundcloud · @lethys · rss