Avoiding HTML injections and Cross Site Attacks on Input Fields
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:
…and this template:
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:(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:
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 aftermydata = "
. 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 loadsevil.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 thesafe
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:
Then we’d not be open to the above attack. But the data would not render as intended:
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:…and this template:
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 makemydata_json
equal to:(Extra double quotes from
json.dumps
, which converted it into a JSON string stored in a Python string.)Then the template would render to:
Again we have the same problem. The browser will parse the HTML as an incomplete
<script>
, then another<script>
to includeevil.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:
This will get rendered like so:
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:
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
:And then refer to it in the template:
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: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.
Comments
Post a Comment