![]() |
VOOZH | about |
20 December 2018
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!
_includes/json.html:
JSON.parse(decodeURIComponent("{{ include.data | jsonify | uri_escape }}"))
So you can now do: {% include json.html data=foo %}
_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?
is a software engineer living in NYC who is building
Superset 💪
and also created
GoFullPage 📸
more »
github · soundcloud · rss
mrcoles.com © 2026