CVE-2013-6044: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: The is_safe_url function in utils/http.py in Django 1.4.x before 1.4.6, 1.5.x before 1.5.2, and 1.6 before beta 2 treats a URL's scheme as safe even if it is not HTTP or HTTPS, which might introduce cross-site scripting (XSS) or other vulnerabilities into Django applications that use this function, as demonstrated by "the login view in django.contrib.auth.views" and the javascript: scheme.
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: 2013-10-04T17:55Z
Repository: https://github.com/django/django
Language: Python
Files changed: django/contrib/auth/tests/test_views.py, django/utils/http.py
Commit date: 2013-08-13 11:06:22 -0500
Commit message: Fixed is_safe_url() to reject URLs that use a scheme other than HTTP/S.

This is a security fix; disclosure to follow shortly.

Diff: diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py
index 22ccbfd225..7839b0b9f9 100644
--- a/django/contrib/auth/tests/test_views.py
+++ b/django/contrib/auth/tests/test_views.py
@@ -444,11 +444,12 @@ class LoginTest(AuthViewsTestCase):
 
         # Those URLs should not pass the security check
         for bad_url in ('http://example.com',
                         'https://example.com',
                         'ftp://exampel.com',
-                        '//example.com'):
+                        '//example.com',
+                        'javascript:alert("XSS")'):
 
             nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
                 'url': login_url,
                 'next': REDIRECT_FIELD_NAME,
                 'bad_url': urlquote(bad_url),
@@ -465,10 +466,11 @@ class LoginTest(AuthViewsTestCase):
         for good_url in ('/view/?param=http://example.com',
                          '/view/?param=https://example.com',
                          '/view?param=ftp://exampel.com',
                          'view/?param=//example.com',
                          'https:///',
+                         'HTTPS:///',
                          '//testserver/',
                          '/url%20with%20spaces/'):  # see ticket #12534
             safe_url = '%(url)s?%(next)s=%(good_url)s' % {
                 'url': login_url,
                 'next': REDIRECT_FIELD_NAME,
@@ -659,11 +661,12 @@ class LogoutTest(AuthViewsTestCase):
 
         # Those URLs should not pass the security check
         for bad_url in ('http://example.com',
                         'https://example.com',
                         'ftp://exampel.com',
-                        '//example.com'):
+                        '//example.com',
+                        'javascript:alert("XSS")'):
             nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
                 'url': logout_url,
                 'next': REDIRECT_FIELD_NAME,
                 'bad_url': urlquote(bad_url),
             }
@@ -678,10 +681,11 @@ class LogoutTest(AuthViewsTestCase):
         for good_url in ('/view/?param=http://example.com',
                          '/view/?param=https://example.com',
                          '/view?param=ftp://exampel.com',
                          'view/?param=//example.com',
                          'https:///',
+                         'HTTPS:///',
                          '//testserver/',
                          '/url%20with%20spaces/'):  # see ticket #12534
             safe_url = '%(url)s?%(next)s=%(good_url)s' % {
                 'url': logout_url,
                 'next': REDIRECT_FIELD_NAME,
diff --git a/django/utils/http.py b/django/utils/http.py
index e397acad5b..9b36ab91d7 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -250,13 +250,14 @@ def same_origin(url1, url2):
         return False
 
 def is_safe_url(url, host=None):
     """
     Return ``True`` if the url is a safe redirection (i.e. it doesn't point to
-    a different host).
+    a different host and uses a safe scheme).
 
     Always returns ``False`` on an empty url.
     """
     if not url:
         return False
-    netloc = urllib_parse.urlparse(url)[1]
-    return not netloc or netloc == host
+    url_info = urllib_parse.urlparse(url)
+    return (not url_info.netloc or url_info.netloc == host) and \
+        (not url_info.scheme or url_info.scheme in ['http', 'https'])

Vulnerable code:                         '//example.com'):
                        '//example.com'):
    a different host).
    netloc = urllib_parse.urlparse(url)[1]
    return not netloc or netloc == host
Fixed code:                         '//example.com',
                        'javascript:alert("XSS")'):
                         'HTTPS:///',
                        '//example.com',
                        'javascript:alert("XSS")'):
                         'HTTPS:///',
    a different host and uses a safe scheme).
    url_info = urllib_parse.urlparse(url)
    return (not url_info.netloc or url_info.netloc == host) and \
        (not url_info.scheme or url_info.scheme in ['http', 'https'])