Repository: web2py/web2py

Issue:
Stored XSS / Frame injection
In (probably) all versions including latest 2.18.5 is it possible to find one example file which is vulnerable to XSS(reflected, stored) and frame injection. (I do not want to publicly disclose attack vector and specific file because is it still possible to exploit)




Discussion:

Resolving patch:
diff --git a/gluon/serializers.py b/gluon/serializers.py
--- a/gluon/serializers.py
+++ b/gluon/serializers.py
@@ -119,13 +119,43 @@ def xml(value, encoding='UTF-8', key='document', quote=True):
     return ('<?xml version="1.0" encoding="%s"?>' % encoding) + str(xml_rec(value, key, quote))
 
 
-def json(value, default=custom_json, indent=None, sort_keys=False):
-    value = json_parser.dumps(value, default=default, sort_keys=sort_keys, indent=indent)
-    # replace JavaScript incompatible spacing
-    # http://timelessrepo.com/json-isnt-a-javascript-subset
-    # PY3 FIXME
-    # return value.replace(ur'\u2028', '\\u2028').replace(ur'\2029', '\\u2029')
-    return value
+class JSONEncoderForHTML(json_parser.JSONEncoder):
+    """An encoder that produces JSON safe to embed in HTML.
+    To embed JSON content in, say, a script tag on a web page, the
+    characters &, < and > should be escaped. They cannot be escaped
+    with the usual entities (e.g. &amp;) because they are not expanded
+    within <script> tags.
+    This class also escapes the line separator and paragraph separator
+    characters U+2028 and U+2029, irrespective of the ensure_ascii setting,
+    as these characters are not valid in JavaScript strings (see
+    http://timelessrepo.com/json-isnt-a-javascript-subset).
+    """
+
+    def encode(self, o):
+        # Override JSONEncoder.encode because it has hacks for
+        # performance that make things more complicated.
+        chunks = self.iterencode(o, True)
+        if self.ensure_ascii:
+            return ''.join(chunks)
+        else:
+            return u''.join(chunks)
+
+    def iterencode(self, o, _one_shot=False):
+        chunks = super(JSONEncoderForHTML, self).iterencode(o, _one_shot)
+        for chunk in chunks:
+            chunk = chunk.replace('&', '\\u0026')
+            chunk = chunk.replace('<', '\\u003c')
+            chunk = chunk.replace('>', '\\u003e')
+
+            if not self.ensure_ascii:
+                chunk = chunk.replace(u'\u2028', '\\u2028')
+                chunk = chunk.replace(u'\u2029', '\\u2029')
+
+            yield chunk
+
+
+def json(value, default=custom_json, indent=None, sort_keys=False, cls=JSONEncoderForHTML):
+    return json_parser.dumps(value, default=default, cls=cls, sort_keys=sort_keys, indent=indent)
 
 def csv(value):
     return ''


Test patch:
diff --git a/gluon/tests/test_serializers.py b/gluon/tests/test_serializers.py
--- a/gluon/tests/test_serializers.py
+++ b/gluon/tests/test_serializers.py
@@ -50,7 +50,8 @@ def testJSON(self):
         lazy_translation = T('abc')
         self.assertEqual(json(lazy_translation), u'"abc"')
         # html helpers are xml()ed before too
-        self.assertEqual(json(SPAN('abc')), u'"<span>abc</span>"')
+        self.assertEqual(json(SPAN('abc'), cls=None), u'"<span>abc</span>"')
+        self.assertEqual(json(SPAN('abc')), u'"\\u003cspan\\u003eabc\\u003c/span\\u003e"')
         # unicode keys make a difference with loads_json
         base = {u'è': 1, 'b': 2}
         base_enc = json(base)