Search code examples
javascripthtmljsonnunjucks

How to safely render JSON into an inline <script> using Nunjucks?


We have a server-side rendered HTML page which includes an external JavaScript file. To trigger the code in that file, we want to call a function and pass it some dynamic data (in the form of JSON):

<script src="/static/foo/bar.js"></script>
<script>
  foo.bar.init({biz: 42, qux: "quux"});
</script>

We're rendering this from a Nunjucks template, and passing the JSON object as a value data in the context. This can contain arbitrary data, including user-provided content.

This is safe but does not work, because <>& are being escaped (thanks to Nunjucks autoescaping):

foo.bar.init({{ data | dump }});

This works, but is not safe, because a string in the JSON might contain the text </script>:

foo.bar.init({{ data | dump | safe }});

How to convince Nunjucks to render this JSON so it can be interpreted safely and correctly? It sounds like it should be a solved problem, but I can't find a premade solution anywhere.


Solution

  • Doing this for now:

      /**
       * Returns a JSON stringified version of the value, safe for inclusion in an
       * inline <script> tag. The optional argument 'spaces' can be used for
       * pretty-printing.
       *
       * Output is NOT safe for inclusion in HTML! If that's what you need, use the
       * built-in 'dump' filter instead.
       */
      env.addFilter('json', function (value, spaces) {
        if (value instanceof nunjucks.runtime.SafeString) {
          value = value.toString()
        }
        const jsonString = JSON.stringify(value, null, spaces).replace(/</g, '\\u003c')
        return nunjucks.runtime.markSafe(jsonString)
      })
    

    Usage:

    <script src="/static/foo/bar.js"></script>
    <script>
      foo.bar.init({{ data | json }});
    </script>
    

    Better solutions still welcome.