summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonas Haag <jonas@lophus.org>2017-10-12 20:59:38 +0200
committerTim Graham <timograham@gmail.com>2018-02-07 18:38:12 -0500
commit8c709d79cbd1a7bb975f58090c17a1178a0efb80 (patch)
tree79e6be4dac2289ca5e4943db9ae071d14f95cdc5
parentef2512b2ffdb719e5c0fb82142f9ce8478282823 (diff)
Fixed #17419 -- Added json_tag template filter.
-rw-r--r--django/template/defaultfilters.py13
-rw-r--r--django/utils/html.py22
-rw-r--r--docs/ref/templates/builtins.txt40
-rw-r--r--docs/releases/2.1.txt3
-rw-r--r--tests/template_tests/filter_tests/test_json_script.py19
-rw-r--r--tests/utils_tests/test_html.py26
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>&amp;'}``, 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/'),