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?