21

Safely Including Data for JavaScript in a Django Template

 4 years ago
source link: https://adamj.eu/tech/2020/02/18/safely-including-data-for-javascript-in-a-django-template/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2020-02-18-syringe.jpg

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.

Imagine get_mydata() returned this crafty string:

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

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 = "</script><script src="https://example.com/evil.js"></script>";
</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!

Fin

I hope this helps you write more secure Django applications. Thanks to Jonas Haag for implementing json_script in Django 2.1, and Tim Graham for reviewing it.

—Adam

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK