Django’s escapejs, its security implications, and alternatives

What’s escapejs?

Anyone who already knows what escapejs is can probably skip this section, but if you’re not familiar with Django, or if you just started and stumbled upon this, I’ll give some context.

Django has two common templating systems [1], you don’t have to use them, but for most cases, you’d have to be pretty crazy not too. One of the most important things they provide for you is that they automatically escape any text that you output into your HTML. This is really valuable because it protects you from a class of exploits called XSS attacks [WP2019]. Apart from that, the Django templating system is a pretty easy way to produce dynamic HTML without understanding much about programming. Anyone experienced enough to write HTML is probably clever enough to manipulate these templates, which means that you can have web designers working directly with the actual code, which can save a lot of time and let you build your product faster.

Now, the good news is that these templates make it really easy to output dynamic text, HTML, and in some cases XML and CSV [2]. The bad news, is that it’s rather hard to use them for any other formats. One of these other formats is JavaScript [3].

To get a sense of what’s going on, consider the following code that uses c3.js [CT2019] to make a chart of menu items and how many of them are sold.

var chart = c3.generate({
    data: {
        columns: [
            ["Burgers", 30],
            ["Fish & Chips", 10]
        ],
        type : "pie",
    }
});

It’s fairly common to want to embed this sort of code directly into HTML. You might want to dynamically create this chart, and write a template like this:

var chart = c3.generate({
    data: {
        columns: [
          {% for row in list %}
            ["{{ row.name }}", {{ row.number_sold }}],
          {% endfor %}
        ],
        type : "pie",
    }
});

If you tried doing this, you’d probably end up noticing fairly quickly that instead of “Fish & Chips" you have “Fish & Chips”. escapejs is a solution to that problem. In this case, if you replace row.name with row.name|escapejs you get the result you wanted.

What escapejs is good for

The escapejs filter is good for anything inside a plain JavaScript string. If you try to use it outside of a string, you’re probably going to get a syntax error. Importantly, if you try using it in a JavaScript template, you’ll get a potential XSS bug.

This is safe:

var text = "Hello {{ text|escapejs }}";

This is unsafe:

var hello = "Hello";
var text = `${hello} {{ text|escapejs }}`;

Note that the difference is subtle. Templates are a new addition to JavaScript, so you may not have seen them before. More importantly, it’s probably not good to expect template authors to understand the difference between these two. As a result, I’d generally recommend against using escapejs and mixing JavaScript with HTML content. I made a sample Django app that shows this problem [AT2019].

What to absolutely not do

Depending on what data you’re testing with, there’s a few other solutions that look like they might work well, but can actually leave huge security holes.

  1. Don’t use autoescape off or safe. These will completely disable any sort of automatic escaping and if any of your dynamic output is not completely controlled, you can easily end up bad output or an XSS exploit.

  2. Don’t use addslashes, you might see this in old Django code, but it’s insecure in JavaScript code [DJ2007].

If you find yourself wanting to dump a huge blob of JSON in to your page, try to think of another option. We’ll talk about an alternative in the next section.

A better alternative

One of the fundamental ideas of the design around HTML was to preserve a separation between the data (your HTML document) and the how that data was displayed. As it turns out, 99 times out of 100, the dynamic stuff that we want to output is data. As you might guess, a good way handle this, is to just store the data as HTML and then use a separate piece of JavaScript to transform your HTML into whatever nifty thing you want. You can store your data in two locations:

  • Inside HTML elements, like a table or a list

  • Inside data attributes

This also has three advantages to the previous approaches

  • It fixes your escaping problems

  • It works on browsers without JavaScript

  • It is accessible (and better for SEO, if that matters)

Here’s an example using the chart example from above:

<dl id="data">
  {% for row in list %}
    <dt>{{ row.name }}</dt><dd>{{ row.number_sold }}</dd>
  {% endfor %}
</dl>
var el = document.getElementById("data");
var columns = [];
var titles = el.getElementsByTagName("dt");
for (var i = 0; i < titles.length; i++) {
    var title = titles[i];
    columns.push([title.textContent,
                  title.nextElementSibling.textContent]);
}
el.style.display = "none";

var chart = c3.generate({
    data: {
        columns: columns,
        type : "pie",
    }
});

There you have it, and you can put that JavaScript in a separate file if you like too.

Footnotes

References

[WP2019] Wikipedia contributors. Cross-site scripting. (retrieved 2019-6-30).