CVE-2014-3995: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site scripting (XSS) vulnerability in gravatars/templatetags/gravatars.py in Djblets before 0.7.30 and 0.8.x before 0.8.3 for Django allows remote attackers to inject arbitrary web script or HTML via a user display 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/gravatars/templatetags/gravatars.py, djblets/gravatars/templatetags/tests.py
Commit date: 2014-06-06 12:54:10 -0700
Commit message: Fix a XSS vulnerability in the gravatar template tag.

The generated gravatar HTML wasn't handling escaping of the display name
of the user, allowing an attacker to choose a name that would close out
the <img> tag and inject a <script> tag.

By switching to Django's format_html(), we can guarantee safe escaping
of content.

Diff: diff --git a/djblets/gravatars/templatetags/gravatars.py b/djblets/gravatars/templatetags/gravatars.py
index fc68cca8..699d2a07 100644
--- a/djblets/gravatars/templatetags/gravatars.py
+++ b/djblets/gravatars/templatetags/gravatars.py
@@ -23,10 +23,11 @@
 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 from __future__ import unicode_literals
 
 from django import template
+from django.utils.html import format_html
 
 from djblets.gravatars import (get_gravatar_url,
                                get_gravatar_url_for_email)
 from djblets.util.decorators import basictag
 
@@ -53,13 +54,14 @@ def gravatar(context, user, size=None):
     See http://www.gravatar.com/ for more information.
     """
     url = get_gravatar_url(context['request'], user, size)
 
     if url:
-        return ('<img src="%s" width="%s" height="%s" alt="%s" '
-                '     class="gravatar"/>' %
-                (url, size, size, user.get_full_name() or user.username))
+        return format_html(
+            '<img src="{0}" width="{1}" height="{1}" alt="{2}" '
+            'class="gravatar"/>',
+            url, size, user.get_full_name() or user.username)
     else:
         return ''
 
 
 @register.tag
diff --git a/djblets/gravatars/templatetags/tests.py b/djblets/gravatars/templatetags/tests.py
new file mode 100644
index 00000000..69c237d5
--- /dev/null
+++ b/djblets/gravatars/templatetags/tests.py
@@ -0,0 +1,34 @@
+from __future__ import unicode_literals
+
+from django.contrib.auth.models import User
+from django.template import Token, TOKEN_TEXT
+
+from djblets.testing.testcases import TagTest
+from djblets.gravatars.templatetags.gravatars import gravatar
+
+
+class DummyRequest(object):
+    def is_secure(self):
+        return False
+
+
+class TagTests(TagTest):
+    """Unit tests for gravatars template tags."""
+    def test_gravatar_xss(self):
+        """Testing {% gravatar %} doesn't allow XSS injection"""
+        user = User(username='test',
+                    first_name='"><script>alert(1);</script><"',
+                    email='test@example.com')
+
+        node = gravatar(self.parser, Token(TOKEN_TEXT, 'gravatar user 32'))
+        context = {
+            'request': DummyRequest(),
+            'user': user,
+        }
+
+        self.assertEqual(
+            node.render(context),
+            '<img src="http://www.gravatar.com/avatar/'
+            '55502f40dc8b7c769880b10874abc9d0?s=32" width="32" height="32" '
+            'alt="&quot;&gt;&lt;script&gt;alert(1);&lt;/script&gt;&lt;&quot;" '
+            'class="gravatar"/>')

Vulnerable code:         return ('<img src="%s" width="%s" height="%s" alt="%s" '
                '     class="gravatar"/>' %
                (url, size, size, user.get_full_name() or user.username))
Fixed code: from django.utils.html import format_html
        return format_html(
            '<img src="{0}" width="{1}" height="{1}" alt="{2}" '
            'class="gravatar"/>',
            url, size, user.get_full_name() or user.username)
from __future__ import unicode_literals

from django.contrib.auth.models import User
from django.template import Token, TOKEN_TEXT

from djblets.testing.testcases import TagTest
from djblets.gravatars.templatetags.gravatars import gravatar


class DummyRequest(object):
    def is_secure(self):
        return False


class TagTests(TagTest):
    """Unit tests for gravatars template tags."""
    def test_gravatar_xss(self):
        """Testing {% gravatar %} doesn't allow XSS injection"""
        user = User(username='test',
                    first_name='"><script>alert(1);</script><"',
                    email='test@example.com')

        node = gravatar(self.parser, Token(TOKEN_TEXT, 'gravatar user 32'))
        context = {
            'request': DummyRequest(),
            'user': user,
        }

        self.assertEqual(
            node.render(context),
            '<img src="http://www.gravatar.com/avatar/'
            '55502f40dc8b7c769880b10874abc9d0?s=32" width="32" height="32" '
            'alt="&quot;&gt;&lt;script&gt;alert(1);&lt;/script&gt;&lt;&quot;" '
            'class="gravatar"/>')