CVE-2016-2512: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: The utils.http.is_safe_url function in Django before 1.8.10 and 1.9.x before 1.9.3 allows remote attackers to redirect users to arbitrary web sites and conduct phishing attacks or possibly conduct cross-site scripting (XSS) attacks via a URL containing basic authentication, as demonstrated by http://mysite.example.com\@attacker.com.
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.
CVSS v3 base score: 7.4
Published: 2016-04-08T15:59Z
Repository: https://github.com/django/django
Language: Python
Files changed: django/utils/http.py, docs/releases/1.8.10.txt, docs/releases/1.9.3.txt, tests/utils_tests/test_http.py
Commit date: 2016-03-01 11:25:28 -0500
Commit message: Fixed CVE-2016-2512 -- Prevented spoofing is_safe_url() with basic auth.

This is a security fix.

Diff: diff --git a/django/utils/http.py b/django/utils/http.py
index 2dce7d3add..6e782bd576 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -288,12 +288,16 @@ def is_safe_url(url, host=None):
     """
     if url is not None:
         url = url.strip()
     if not url:
         return False
-    # Chrome treats \ completely as /
-    url = url.replace('\\', '/')
+    # Chrome treats \ completely as / in paths but it could be part of some
+    # basic auth credentials so we need to check both URLs.
+    return _is_safe_url(url, host) and _is_safe_url(url.replace('\\', '/'), host)
+
+
+def _is_safe_url(url, host):
     # Chrome considers any URL with more than two slashes to be absolute, but
     # urlparse is not so flexible. Treat any url with three slashes as unsafe.
     if url.startswith('///'):
         return False
     url_info = urlparse(url)
diff --git a/docs/releases/1.8.10.txt b/docs/releases/1.8.10.txt
index d93f243720..73c7cc04a4 100644
--- a/docs/releases/1.8.10.txt
+++ b/docs/releases/1.8.10.txt
@@ -4,10 +4,26 @@ Django 1.8.10 release notes
 
 *March 1, 2015*
 
 Django 1.8.10 fixes two security issues and several bugs in 1.8.9.
 
+CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redirect URLs containing basic auth
+===============================================================================================================
+
+Django relies on user input in some cases (e.g.
+:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
+to redirect the user to an "on success" URL. The security check for these
+redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
+with basic authentication credentials "safe" when they shouldn't be.
+
+For example, a URL like ``http://mysite.example.com\@attacker.com`` would be
+considered safe if the request's host is ``http://mysite.example.com``, but
+redirecting to this URL sends the user to ``attacker.com``.
+
+Also, if a developer relies on ``is_safe_url()`` to provide safe redirect
+targets and puts such a URL into a link, they could suffer from an XSS attack.
+
 Bugfixes
 ========
 
 * Fixed a crash on PostgreSQL that prevented using ``TIME_ZONE=None`` and
   ``USE_TZ=False`` (:ticket:`26177`).
diff --git a/docs/releases/1.9.3.txt b/docs/releases/1.9.3.txt
index 8cc57d8a81..056122b16e 100644
--- a/docs/releases/1.9.3.txt
+++ b/docs/releases/1.9.3.txt
@@ -4,10 +4,26 @@ Django 1.9.3 release notes
 
 *March 1, 2015*
 
 Django 1.9.3 fixes two security issues and several bugs in 1.9.2.
 
+CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redirect URLs containing basic auth
+===============================================================================================================
+
+Django relies on user input in some cases (e.g.
+:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
+to redirect the user to an "on success" URL. The security check for these
+redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
+with basic authentication credentials "safe" when they shouldn't be.
+
+For example, a URL like ``http://mysite.example.com\@attacker.com`` would be
+considered safe if the request's host is ``http://mysite.example.com``, but
+redirecting to this URL sends the user to ``attacker.com``.
+
+Also, if a developer relies on ``is_safe_url()`` to provide safe redirect
+targets and puts such a URL into a link, they could suffer from an XSS attack.
+
 Bugfixes
 ========
 
 * Skipped URL checks (new in 1.9) if the ``ROOT_URLCONF`` setting isn't defined
   (:ticket:`26155`).
diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
index 6234e269ab..2a9b553b6a 100644
--- a/tests/utils_tests/test_http.py
+++ b/tests/utils_tests/test_http.py
@@ -95,21 +95,33 @@ class TestUtilsHttp(unittest.TestCase):
                         'http:\/example.com',
                         'http:/\example.com',
                         'javascript:alert("XSS")',
                         '\njavascript:alert(x)',
                         '\x08//example.com',
+                        r'http://otherserver\@example.com',
+                        r'http:\\testserver\@example.com',
+                        r'http://testserver\me:pass@example.com',
+                        r'http://testserver\@example.com',
+                        r'http:\\testserver\confirm\me@example.com',
                         '\n'):
             self.assertFalse(http.is_safe_url(bad_url, host='testserver'), "%s should be blocked" % bad_url)
         for good_url in ('/view/?param=http://example.com',
                      '/view/?param=https://example.com',
                      '/view?param=ftp://example.com',
                      'view/?param=//example.com',
                      'https://testserver/',
                      'HTTPS://testserver/',
                      '//testserver/',
+                     'http://testserver/confirm?email=me@example.com',
                      '/url%20with%20spaces/'):
             self.assertTrue(http.is_safe_url(good_url, host='testserver'), "%s should be allowed" % good_url)
+        # Valid basic auth credentials are allowed.
+        self.assertTrue(http.is_safe_url(r'http://user:pass@testserver/', host='user:pass@testserver'))
+        # A path without host is allowed.
+        self.assertTrue(http.is_safe_url('/confirm/me@example.com'))
+        # Basic auth without host is not allowed.
+        self.assertFalse(http.is_safe_url(r'http://testserver\@example.com'))
 
     def test_urlsafe_base64_roundtrip(self):
         bytestring = b'foo'
         encoded = http.urlsafe_base64_encode(bytestring)
         decoded = http.urlsafe_base64_decode(encoded)

Vulnerable code:     # Chrome treats \ completely as /
    url = url.replace('\\', '/')
Fixed code:     # Chrome treats \ completely as / in paths but it could be part of some
    # basic auth credentials so we need to check both URLs.
    return _is_safe_url(url, host) and _is_safe_url(url.replace('\\', '/'), host)


def _is_safe_url(url, host):
CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redirect URLs containing basic auth
===============================================================================================================

Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security check for these
redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
with basic authentication credentials "safe" when they shouldn't be.

For example, a URL like ``http://mysite.example.com\@attacker.com`` would be
considered safe if the request's host is ``http://mysite.example.com``, but
redirecting to this URL sends the user to ``attacker.com``.

Also, if a developer relies on ``is_safe_url()`` to provide safe redirect
targets and puts such a URL into a link, they could suffer from an XSS attack.

CVE-2016-2512: Malicious redirect and possible XSS attack via user-supplied redirect URLs containing basic auth
===============================================================================================================

Django relies on user input in some cases (e.g.
:func:`django.contrib.auth.views.login` and :doc:`i18n </topics/i18n/index>`)
to redirect the user to an "on success" URL. The security check for these
redirects (namely ``django.utils.http.is_safe_url()``) considered some URLs
with basic authentication credentials "safe" when they shouldn't be.

For example, a URL like ``http://mysite.example.com\@attacker.com`` would be
considered safe if the request's host is ``http://mysite.example.com``, but
redirecting to this URL sends the user to ``attacker.com``.

Also, if a developer relies on ``is_safe_url()`` to provide safe redirect
targets and puts such a URL into a link, they could suffer from an XSS attack.

                        r'http://otherserver\@example.com',
                        r'http:\\testserver\@example.com',
                        r'http://testserver\me:pass@example.com',
                        r'http://testserver\@example.com',
                        r'http:\\testserver\confirm\me@example.com',
                     'http://testserver/confirm?email=me@example.com',
        # Valid basic auth credentials are allowed.
        self.assertTrue(http.is_safe_url(r'http://user:pass@testserver/', host='user:pass@testserver'))
        # A path without host is allowed.
        self.assertTrue(http.is_safe_url('/confirm/me@example.com'))
        # Basic auth without host is not allowed.
        self.assertFalse(http.is_safe_url(r'http://testserver\@example.com'))