CVE-2014-3994: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site scripting (XSS) vulnerability in util/templatetags/djblets_js.py in Djblets before 0.7.30 and 0.8.x before 0.8.3 for Django, as used in Review Board, allows remote attackers to inject arbitrary web script or HTML via a JSON object, as demonstrated by the name field when changing a user name.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Published: 2014-06-16T18:55Z
Repository: https://github.com/djblets/djblets
Language: Python
Files changed: djblets/util/templatetags/djblets_js.py, djblets/util/templatetags/tests.py
Commit date: 2014-06-06 12:53:59 -0700
Commit message: Fix a XSS vulnerability with bad input to json_dumps.

Django's JSON serialization does not handle escaping of any characters
to make them safe for injecting into HTML. This allows an attacker who
can provide part of a JSON-serializable object to craft a string that
can break out of a <script> tag and create its own, injecting a custom
script.

To fix this, we escape '<', '>', and '&' characters in the resulting
string, preventing a </script> from executing.

Diff: diff --git a/djblets/util/templatetags/djblets_js.py b/djblets/util/templatetags/djblets_js.py
index 5cb5449..ae9bff8 100644
--- a/djblets/util/templatetags/djblets_js.py
+++ b/djblets/util/templatetags/djblets_js.py
@@ -29,17 +29,24 @@ import json
 
 from django import template
 from django.core.serializers import serialize
 from django.db.models.query import QuerySet
 from django.utils import six
+from django.utils.encoding import force_text
 from django.utils.safestring import mark_safe
 
 from djblets.util.serializers import DjbletsJSONEncoder
 
 
 register = template.Library()
 
+_safe_js_escapes = {
+    ord('&'): '\\u0026',
+    ord('<'): '\\u003C',
+    ord('>'): '\\u003E',
+}
+
 
 @register.simple_tag
 def form_dialog_fields(form):
     """
     Translates a Django Form object into a JavaScript list of fields.
@@ -73,11 +80,11 @@ def json_dumps(value, indent=None):
     if isinstance(value, QuerySet):
         result = serialize('json', value, indent=indent)
     else:
         result = json.dumps(value, indent=indent, cls=DjbletsJSONEncoder)
 
-    return mark_safe(result)
+    return mark_safe(force_text(result).translate(_safe_js_escapes))
 
 
 @register.filter
 def json_dumps_items(d, append=''):
     """Dumps a list of keys/values from a dictionary, without braces.
diff --git a/djblets/util/templatetags/tests.py b/djblets/util/templatetags/tests.py
new file mode 100644
index 0000000..e5281f9
--- /dev/null
+++ b/djblets/util/templatetags/tests.py
@@ -0,0 +1,19 @@
+from __future__ import unicode_literals
+
+from djblets.testing.testcases import TestCase
+from djblets.util.templatetags.djblets_js import json_dumps
+
+
+class JSTagTests(TestCase):
+    """Unit tests for djblets_js template tags."""
+    def test_json_dumps_xss(self):
+        """Testing json_dumps doesn't allow XSS injection"""
+        # This is bug 3406.
+        obj = {
+            'xss': '</script><script>alert(1);</script>'
+        }
+
+        self.assertEqual(
+            json_dumps(obj),
+            '{"xss": "\\u003C/script\\u003E\\u003Cscript\\u003E'
+            'alert(1);\\u003C/script\\u003E"}')

Vulnerable code:     return mark_safe(result)
Fixed code: from django.utils.encoding import force_text
_safe_js_escapes = {
    ord('&'): '\\u0026',
    ord('<'): '\\u003C',
    ord('>'): '\\u003E',
}

    return mark_safe(force_text(result).translate(_safe_js_escapes))
from __future__ import unicode_literals

from djblets.testing.testcases import TestCase
from djblets.util.templatetags.djblets_js import json_dumps


class JSTagTests(TestCase):
    """Unit tests for djblets_js template tags."""
    def test_json_dumps_xss(self):
        """Testing json_dumps doesn't allow XSS injection"""
        # This is bug 3406.
        obj = {
            'xss': '</script><script>alert(1);</script>'
        }

        self.assertEqual(
            json_dumps(obj),
            '{"xss": "\\u003C/script\\u003E\\u003Cscript\\u003E'
            'alert(1);\\u003C/script\\u003E"}')