diff options
| author | Jonas Haag <jonas@lophus.org> | 2017-10-12 20:59:38 +0200 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2018-02-07 18:38:12 -0500 |
| commit | 8c709d79cbd1a7bb975f58090c17a1178a0efb80 (patch) | |
| tree | 79e6be4dac2289ca5e4943db9ae071d14f95cdc5 | |
| parent | ef2512b2ffdb719e5c0fb82142f9ce8478282823 (diff) | |
Fixed #17419 -- Added json_tag template filter.
| -rw-r--r-- | django/template/defaultfilters.py | 13 | ||||
| -rw-r--r-- | django/utils/html.py | 22 | ||||
| -rw-r--r-- | docs/ref/templates/builtins.txt | 40 | ||||
| -rw-r--r-- | docs/releases/2.1.txt | 3 | ||||
| -rw-r--r-- | tests/template_tests/filter_tests/test_json_script.py | 19 | ||||
| -rw-r--r-- | tests/utils_tests/test_html.py | 26 |
6 files changed, 118 insertions, 5 deletions
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index e876c555f3..e32b7f7a5f 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -12,8 +12,8 @@ from django.utils import formats from django.utils.dateformat import format, time_format from django.utils.encoding import iri_to_uri from django.utils.html import ( - avoid_wrapping, conditional_escape, escape, escapejs, linebreaks, - strip_tags, urlize as _urlize, + avoid_wrapping, conditional_escape, escape, escapejs, + json_script as _json_script, linebreaks, strip_tags, urlize as _urlize, ) from django.utils.safestring import SafeData, mark_safe from django.utils.text import ( @@ -83,6 +83,15 @@ def escapejs_filter(value): @register.filter(is_safe=True) +def json_script(value, element_id): + """ + Output value JSON-encoded, wrapped in a <script type="application/json"> + tag. + """ + return _json_script(value, element_id) + + +@register.filter(is_safe=True) def floatformat(text, arg=-1): """ Display a float to a specified number of decimal places. diff --git a/django/utils/html.py b/django/utils/html.py index e6389d4db5..60f505a4b7 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -1,5 +1,6 @@ """HTML utilities suitable for global use.""" +import json import re from html.parser import HTMLParser from urllib.parse import ( @@ -77,6 +78,27 @@ def escapejs(value): return mark_safe(str(value).translate(_js_escapes)) +_json_script_escapes = { + ord('>'): '\\u003E', + ord('<'): '\\u003C', + ord('&'): '\\u0026', +} + + +def json_script(value, element_id): + """ + Escape all the HTML/XML special characters with their unicode escapes, so + value is safe to be output anywhere except for inside a tag attribute. Wrap + the escaped JSON in a script tag. + """ + from django.core.serializers.json import DjangoJSONEncoder + json_str = json.dumps(value, cls=DjangoJSONEncoder).translate(_json_script_escapes) + return format_html( + '<script id="{}" type="application/json">{}</script>', + element_id, mark_safe(json_str) + ) + + def conditional_escape(text): """ Similar to escape(), except that it doesn't operate on pre-escaped strings. diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 634adf42e3..a4e0a0d455 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -1782,6 +1782,46 @@ For example:: If ``value`` is the list ``['a', 'b', 'c']``, the output will be the string ``"a // b // c"``. +.. templatefilter:: json_script + +``json_script`` +--------------- + +.. versionadded:: 2.1 + +Safely outputs a Python object as JSON, wrapped in a ``<script>`` tag, ready +for use with JavaScript. + +**Argument:** HTML "id" of the ``<script>`` tag. + +For example:: + + {{ value|json_script:"hello-data" }} + +If ``value`` is a the dictionary ``{'hello': 'world'}``, the output will be: + +.. code-block:: html + + <script id="hello-data" type="application/json">{"hello": "world"}</script> + +The resulting data can be accessed in JavaScript like this: + +.. code-block:: javascript + + var el = document.getElementById('hello-data'); + var value = JSON.parse(el.textContent || el.innerText); + +XSS attacks are mitigated by escaping the characters "<", ">" and "&". For +example if ``value`` is ``{'hello': 'world</script>&'}``, the output is: + +.. code-block:: html + + <script id="hello-data" type="application/json">{"hello": "world\\u003C/script\\u003E\\u0026amp;"}</script> + +This is compatible with a strict Content Security Policy that prohibits in-page +script execution. It also maintains a clean separation between passive data and +executable code. + .. templatefilter:: last ``last`` diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index 52519170db..ae3dd67bc9 100644 --- a/docs/releases/2.1.txt +++ b/docs/releases/2.1.txt @@ -205,7 +205,8 @@ Signals Templates ~~~~~~~~~ -* ... +* The new :tfilter:`json_script` filter safely outputs a Python object as JSON, + wrapped in a ``<script>`` tag, ready for use with JavaScript. Tests ~~~~~ diff --git a/tests/template_tests/filter_tests/test_json_script.py b/tests/template_tests/filter_tests/test_json_script.py new file mode 100644 index 0000000000..061fe32c12 --- /dev/null +++ b/tests/template_tests/filter_tests/test_json_script.py @@ -0,0 +1,19 @@ +from django.test import SimpleTestCase + +from ..utils import setup + + +class JsonScriptTests(SimpleTestCase): + + @setup({'json-tag01': '{{ value|json_script:"test_id" }}'}) + def test_basic(self): + output = self.engine.render_to_string( + 'json-tag01', + {'value': {'a': 'testing\r\njson \'string" <b>escaping</b>'}} + ) + self.assertEqual( + output, + '<script id="test_id" type="application/json">' + '{"a": "testing\\r\\njson \'string\\" \\u003Cb\\u003Eescaping\\u003C/b\\u003E"}' + '</script>' + ) diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index e6d1fe9a59..e2ebce4556 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -4,8 +4,8 @@ from datetime import datetime from django.test import SimpleTestCase from django.utils.functional import lazystr from django.utils.html import ( - conditional_escape, escape, escapejs, format_html, html_safe, linebreaks, - smart_urlquote, strip_spaces_between_tags, strip_tags, + conditional_escape, escape, escapejs, format_html, html_safe, json_script, + linebreaks, smart_urlquote, strip_spaces_between_tags, strip_tags, ) from django.utils.safestring import mark_safe @@ -147,6 +147,28 @@ class TestUtilsHtml(SimpleTestCase): self.check_output(escapejs, value, output) self.check_output(escapejs, lazystr(value), output) + def test_json_script(self): + tests = ( + # "<", ">" and "&" are quoted inside JSON strings + (('&<>', '<script id="test_id" type="application/json">"\\u0026\\u003C\\u003E"</script>')), + # "<", ">" and "&" are quoted inside JSON objects + ( + {'a': '<script>test&ing</script>'}, + '<script id="test_id" type="application/json">' + '{"a": "\\u003Cscript\\u003Etest\\u0026ing\\u003C/script\\u003E"}</script>' + ), + # Lazy strings are quoted + (lazystr('&<>'), '<script id="test_id" type="application/json">"\\u0026\\u003C\\u003E"</script>'), + ( + {'a': lazystr('<script>test&ing</script>')}, + '<script id="test_id" type="application/json">' + '{"a": "\\u003Cscript\\u003Etest\\u0026ing\\u003C/script\\u003E"}</script>' + ), + ) + for arg, expected in tests: + with self.subTest(arg=arg): + self.assertEqual(json_script(arg, 'test_id'), expected) + def test_smart_urlquote(self): items = ( ('http://öäü.com/', 'http://xn--4ca9at.com/'), |
