Bug 1102680 (CVE-2018-14574)

Summary: VUL-0: CVE-2018-14574: python-Django: Open redirect possibility in ``CommonMiddleware``
Product: [Novell Products] SUSE Security Incidents Reporter: Karol Babioch <karol>
Component: IncidentsAssignee: Alberto Planas Dominguez <aplanas>
Status: RESOLVED FIXED QA Contact: Security Team bot <security-team>
Severity: Normal    
Priority: P3 - Medium CC: atoptsoglou, cloud-bugs, kberger, osukup, smash_bz
Version: unspecified   
Target Milestone: ---   
Hardware: Other   
OS: Other   
URL: https://smash.suse.de/issue/211723/
Whiteboard: https://bugzilla.suse.com/index.cgiCVSSv3:SUSE:CVE-2018-14574:4.2:(AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:L) CVSSv3:RedHat:CVE-2018-14574:4.7:(AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:L/A:N)
Found By: Security Response Team Services Priority:
Business Priority: Blocker: ---
Marketing QA Status: --- IT Deployment: ---

Description Karol Babioch 2018-07-26 05:48:23 UTC
In accordance with that policy, a set of security releases will be
issued on Wednesday, August 1, 2018 around 1400 UTC. This message
contains descriptions of the issues, descriptions of the changes which
will be made to Django, and the patches which will be applied to Django.

CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
=================================================================

If the ``django.middleware.common.CommonMiddleware`` and the
``APPEND_SLASH`` setting are both enabled, and if the project has a URL
pattern that accepts any path ending in a slash (many content management
systems have such a pattern), then a request to a maliciously crafted
URL of that site could lead to a redirect to another site, enabling
phishing and other attacks.

Affected versions
=================

* Django master development branch
* Django 2.1 (currently at release candidate status)
* Django 2.0
* Django 1.11

Resolution
==========

Included with this email are patches implementing the changes described
above for each affected version of Django. On the release date, these
patches will be applied to the Django development repository and the
following releases will be issued along with disclosure of the issues:

* Django 2.1
* Django 2.0.8
* Django 1.11.15

[1] https://www.djangoproject.com/security/


master.diff

commit 0b10bfd837758a35d6eec63b208f3136c14edc58
Author: Andreas Hug <andreas.hug@moccu.com>
Date:   Tue Jul 24 16:18:17 2018 -0400

    Fixed CVE-2018-14574 -- Fixed open redirect possibility in CommonMiddleware.

diff --git a/django/middleware/common.py b/django/middleware/common.py
index bea3f74..a18fbe7 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -7,6 +7,7 @@ from django.core.mail import mail_managers
 from django.http import HttpResponsePermanentRedirect
 from django.urls import is_valid_path
 from django.utils.deprecation import MiddlewareMixin
+from django.utils.http import escape_leading_slashes
 
 
 class CommonMiddleware(MiddlewareMixin):
@@ -79,6 +80,8 @@ class CommonMiddleware(MiddlewareMixin):
         POST, PUT, or PATCH.
         """
         new_path = request.get_full_path(force_append_slash=True)
+        # Prevent construction of scheme relative urls.
+        new_path = escape_leading_slashes(new_path)
         if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
             raise RuntimeError(
                 "You called this URL via %(method)s, but the URL doesn't end "
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index ce8c7ff..5bfab0c 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -17,7 +17,7 @@ from django.core.checks.urls import check_resolver
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.datastructures import MultiValueDict
 from django.utils.functional import cached_property
-from django.utils.http import RFC3986_SUBDELIMS
+from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
 from django.utils.regex_helper import normalize
 from django.utils.translation import get_language
 
@@ -592,9 +592,7 @@ class URLResolver:
                     # safe characters from `pchar` definition of RFC 3986
                     url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
                     # Don't allow construction of scheme relative urls.
-                    if url.startswith('//'):
-                        url = '/%%2F%s' % url[2:]
-                    return url
+                    return escape_leading_slashes(url)
         # lookup_view can be URL name or callable, but callables are not
         # friendly in error messages.
         m = getattr(lookup_view, '__module__', None)
diff --git a/django/utils/http.py b/django/utils/http.py
index caaab4f..5a063a9 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -435,3 +435,14 @@ def limited_parse_qsl(qs, keep_blank_values=False, encoding='utf-8',
             value = unquote(value, encoding=encoding, errors=errors)
             r.append((name, value))
     return r
+
+
+def escape_leading_slashes(url):
+    """
+    If redirecting to an absolute path (two leading slashes), a slash must be
+    escaped to prevent browsers from handling the path as schemaless and
+    redirecting to another host.
+    """
+    if url.startswith('//'):
+        url = '/%2F{}'.format(url[2:])
+    return url
diff --git a/docs/releases/1.11.15.txt b/docs/releases/1.11.15.txt
index 397681d..fca551e 100644
--- a/docs/releases/1.11.15.txt
+++ b/docs/releases/1.11.15.txt
@@ -5,3 +5,16 @@ Django 1.11.15 release notes
 *August 1, 2018*
 
 Django 1.11.15 fixes a security issue in 1.11.14.
+
+CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
+=================================================================
+
+If the :class:`~django.middleware.common.CommonMiddleware` and the
+:setting:`APPEND_SLASH` setting are both enabled, and if the project has a
+URL pattern that accepts any path ending in a slash (many content management
+systems have such a pattern), then a request to a maliciously crafted URL of
+that site could lead to a redirect to another site, enabling phishing and other
+attacks.
+
+``CommonMiddleware`` now escapes leading slashes to prevent redirects to other
+domains.
diff --git a/docs/releases/2.0.8.txt b/docs/releases/2.0.8.txt
index aa47389..167d998 100644
--- a/docs/releases/2.0.8.txt
+++ b/docs/releases/2.0.8.txt
@@ -6,6 +6,19 @@ Django 2.0.8 release notes
 
 Django 2.0.8 fixes a security issue and a bug in 2.0.7.
 
+CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
+=================================================================
+
+If the :class:`~django.middleware.common.CommonMiddleware` and the
+:setting:`APPEND_SLASH` setting are both enabled, and if the project has a
+URL pattern that accepts any path ending in a slash (many content management
+systems have such a pattern), then a request to a maliciously crafted URL of
+that site could lead to a redirect to another site, enabling phishing and other
+attacks.
+
+``CommonMiddleware`` now escapes leading slashes to prevent redirects to other
+domains.
+
 Bugfixes
 ========
 
diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py
index f3c8b9c..88e3334 100644
--- a/tests/middleware/tests.py
+++ b/tests/middleware/tests.py
@@ -130,6 +130,25 @@ class CommonMiddlewareTest(SimpleTestCase):
         self.assertEqual(r.status_code, 301)
         self.assertEqual(r.url, '/needsquoting%23/')
 
+    @override_settings(APPEND_SLASH=True)
+    def test_append_slash_leading_slashes(self):
+        """
+        Paths starting with two slashes are escaped to prevent open redirects.
+        If there's a URL pattern that allows paths to start with two slashes, a
+        request with path //evil.com must not redirect to //evil.com/ (appended
+        slash) which is a schemaless absolute URL. The browser would navigate
+        to evil.com/.
+        """
+        # Use 4 slashes because of RequestFactory behavior.
+        request = self.rf.get('////evil.com/security')
+        response = HttpResponseNotFound()
+        r = CommonMiddleware().process_request(request)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+        r = CommonMiddleware().process_response(request, response)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+
     @override_settings(APPEND_SLASH=False, PREPEND_WWW=True)
     def test_prepend_www(self):
         request = self.rf.get('/path/')
diff --git a/tests/middleware/urls.py b/tests/middleware/urls.py
index 8c6621d..d623e7d 100644
--- a/tests/middleware/urls.py
+++ b/tests/middleware/urls.py
@@ -6,4 +6,6 @@ urlpatterns = [
     url(r'^noslash$', views.empty_view),
     url(r'^slash/$', views.empty_view),
     url(r'^needsquoting#/$', views.empty_view),
+    # Accepts paths with two leading slashes.
+    url(r'^(.+)/security/$', views.empty_view),
 ]
diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
index 05b43c8..1cbb0d9 100644
--- a/tests/utils_tests/test_http.py
+++ b/tests/utils_tests/test_http.py
@@ -5,10 +5,10 @@ from django.test import SimpleTestCase, ignore_warnings
 from django.utils.datastructures import MultiValueDict
 from django.utils.deprecation import RemovedInDjango30Warning
 from django.utils.http import (
-    base36_to_int, cookie_date, http_date, int_to_base36, is_safe_url,
-    is_same_domain, parse_etags, parse_http_date, quote_etag, urlencode,
-    urlquote, urlquote_plus, urlsafe_base64_decode, urlsafe_base64_encode,
-    urlunquote, urlunquote_plus,
+    base36_to_int, cookie_date, escape_leading_slashes, http_date,
+    int_to_base36, is_safe_url, is_same_domain, parse_etags, parse_http_date,
+    quote_etag, urlencode, urlquote, urlquote_plus, urlsafe_base64_decode,
+    urlsafe_base64_encode, urlunquote, urlunquote_plus,
 )
 
 
@@ -275,3 +275,14 @@ class HttpDateProcessingTests(unittest.TestCase):
     def test_parsing_asctime(self):
         parsed = parse_http_date('Sun Nov  6 08:49:37 1994')
         self.assertEqual(datetime.utcfromtimestamp(parsed), datetime(1994, 11, 6, 8, 49, 37))
+
+
+class EscapeLeadingSlashesTests(unittest.TestCase):
+    def test(self):
+        tests = (
+            ('//example.com', '/%2Fexample.com'),
+            ('//', '/%2F'),
+        )
+        for url, expected in tests:
+            with self.subTest(url=url):
+                self.assertEqual(escape_leading_slashes(url), expected)


2.1.x.diff

commit 596375545432e4ceef585ee1de43369b13244466
Author: Andreas Hug <andreas.hug@moccu.com>
Date:   Tue Jul 24 16:18:17 2018 -0400

    [2.1.x] Fixed CVE-2018-14574 -- Fixed open redirect possibility in CommonMiddleware.

diff --git a/django/middleware/common.py b/django/middleware/common.py
index bea3f74..a18fbe7 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -7,6 +7,7 @@ from django.core.mail import mail_managers
 from django.http import HttpResponsePermanentRedirect
 from django.urls import is_valid_path
 from django.utils.deprecation import MiddlewareMixin
+from django.utils.http import escape_leading_slashes
 
 
 class CommonMiddleware(MiddlewareMixin):
@@ -79,6 +80,8 @@ class CommonMiddleware(MiddlewareMixin):
         POST, PUT, or PATCH.
         """
         new_path = request.get_full_path(force_append_slash=True)
+        # Prevent construction of scheme relative urls.
+        new_path = escape_leading_slashes(new_path)
         if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
             raise RuntimeError(
                 "You called this URL via %(method)s, but the URL doesn't end "
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index ce8c7ff..5bfab0c 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -17,7 +17,7 @@ from django.core.checks.urls import check_resolver
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.datastructures import MultiValueDict
 from django.utils.functional import cached_property
-from django.utils.http import RFC3986_SUBDELIMS
+from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
 from django.utils.regex_helper import normalize
 from django.utils.translation import get_language
 
@@ -592,9 +592,7 @@ class URLResolver:
                     # safe characters from `pchar` definition of RFC 3986
                     url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
                     # Don't allow construction of scheme relative urls.
-                    if url.startswith('//'):
-                        url = '/%%2F%s' % url[2:]
-                    return url
+                    return escape_leading_slashes(url)
         # lookup_view can be URL name or callable, but callables are not
         # friendly in error messages.
         m = getattr(lookup_view, '__module__', None)
diff --git a/django/utils/http.py b/django/utils/http.py
index 4558c68..e33ec73 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -433,3 +433,14 @@ def limited_parse_qsl(qs, keep_blank_values=False, encoding='utf-8',
             value = unquote(value, encoding=encoding, errors=errors)
             r.append((name, value))
     return r
+
+
+def escape_leading_slashes(url):
+    """
+    If redirecting to an absolute path (two leading slashes), a slash must be
+    escaped to prevent browsers from handling the path as schemaless and
+    redirecting to another host.
+    """
+    if url.startswith('//'):
+        url = '/%2F{}'.format(url[2:])
+    return url
diff --git a/docs/releases/1.11.15.txt b/docs/releases/1.11.15.txt
index 397681d..fca551e 100644
--- a/docs/releases/1.11.15.txt
+++ b/docs/releases/1.11.15.txt
@@ -5,3 +5,16 @@ Django 1.11.15 release notes
 *August 1, 2018*
 
 Django 1.11.15 fixes a security issue in 1.11.14.
+
+CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
+=================================================================
+
+If the :class:`~django.middleware.common.CommonMiddleware` and the
+:setting:`APPEND_SLASH` setting are both enabled, and if the project has a
+URL pattern that accepts any path ending in a slash (many content management
+systems have such a pattern), then a request to a maliciously crafted URL of
+that site could lead to a redirect to another site, enabling phishing and other
+attacks.
+
+``CommonMiddleware`` now escapes leading slashes to prevent redirects to other
+domains.
diff --git a/docs/releases/2.0.8.txt b/docs/releases/2.0.8.txt
index aa47389..167d998 100644
--- a/docs/releases/2.0.8.txt
+++ b/docs/releases/2.0.8.txt
@@ -6,6 +6,19 @@ Django 2.0.8 release notes
 
 Django 2.0.8 fixes a security issue and a bug in 2.0.7.
 
+CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
+=================================================================
+
+If the :class:`~django.middleware.common.CommonMiddleware` and the
+:setting:`APPEND_SLASH` setting are both enabled, and if the project has a
+URL pattern that accepts any path ending in a slash (many content management
+systems have such a pattern), then a request to a maliciously crafted URL of
+that site could lead to a redirect to another site, enabling phishing and other
+attacks.
+
+``CommonMiddleware`` now escapes leading slashes to prevent redirects to other
+domains.
+
 Bugfixes
 ========
 
diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py
index f3c8b9c..88e3334 100644
--- a/tests/middleware/tests.py
+++ b/tests/middleware/tests.py
@@ -130,6 +130,25 @@ class CommonMiddlewareTest(SimpleTestCase):
         self.assertEqual(r.status_code, 301)
         self.assertEqual(r.url, '/needsquoting%23/')
 
+    @override_settings(APPEND_SLASH=True)
+    def test_append_slash_leading_slashes(self):
+        """
+        Paths starting with two slashes are escaped to prevent open redirects.
+        If there's a URL pattern that allows paths to start with two slashes, a
+        request with path //evil.com must not redirect to //evil.com/ (appended
+        slash) which is a schemaless absolute URL. The browser would navigate
+        to evil.com/.
+        """
+        # Use 4 slashes because of RequestFactory behavior.
+        request = self.rf.get('////evil.com/security')
+        response = HttpResponseNotFound()
+        r = CommonMiddleware().process_request(request)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+        r = CommonMiddleware().process_response(request, response)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+
     @override_settings(APPEND_SLASH=False, PREPEND_WWW=True)
     def test_prepend_www(self):
         request = self.rf.get('/path/')
diff --git a/tests/middleware/urls.py b/tests/middleware/urls.py
index 8c6621d..d623e7d 100644
--- a/tests/middleware/urls.py
+++ b/tests/middleware/urls.py
@@ -6,4 +6,6 @@ urlpatterns = [
     url(r'^noslash$', views.empty_view),
     url(r'^slash/$', views.empty_view),
     url(r'^needsquoting#/$', views.empty_view),
+    # Accepts paths with two leading slashes.
+    url(r'^(.+)/security/$', views.empty_view),
 ]
diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
index 86fcff9..d2a68cc 100644
--- a/tests/utils_tests/test_http.py
+++ b/tests/utils_tests/test_http.py
@@ -5,10 +5,10 @@ from django.test import SimpleTestCase, ignore_warnings
 from django.utils.datastructures import MultiValueDict
 from django.utils.deprecation import RemovedInDjango30Warning
 from django.utils.http import (
-    base36_to_int, cookie_date, http_date, int_to_base36, is_safe_url,
-    is_same_domain, parse_etags, parse_http_date, quote_etag, urlencode,
-    urlquote, urlquote_plus, urlsafe_base64_decode, urlsafe_base64_encode,
-    urlunquote, urlunquote_plus,
+    base36_to_int, cookie_date, escape_leading_slashes, http_date,
+    int_to_base36, is_safe_url, is_same_domain, parse_etags, parse_http_date,
+    quote_etag, urlencode, urlquote, urlquote_plus, urlsafe_base64_decode,
+    urlsafe_base64_encode, urlunquote, urlunquote_plus,
 )
 
 
@@ -271,3 +271,14 @@ class HttpDateProcessingTests(unittest.TestCase):
     def test_parsing_asctime(self):
         parsed = parse_http_date('Sun Nov  6 08:49:37 1994')
         self.assertEqual(datetime.utcfromtimestamp(parsed), datetime(1994, 11, 6, 8, 49, 37))
+
+
+class EscapeLeadingSlashesTests(unittest.TestCase):
+    def test(self):
+        tests = (
+            ('//example.com', '/%2Fexample.com'),
+            ('//', '/%2F'),
+        )
+        for url, expected in tests:
+            with self.subTest(url=url):
+                self.assertEqual(escape_leading_slashes(url), expected)


2.0.x.diff

commit 8330546b40124ff4f79739529310f51075284195
Author: Andreas Hug <andreas.hug@moccu.com>
Date:   Tue Jul 24 16:18:17 2018 -0400

    [2.0.x] Fixed CVE-2018-14574 -- Fixed open redirect possibility in CommonMiddleware.

diff --git a/django/middleware/common.py b/django/middleware/common.py
index d8cfb9a..ea5e536 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -11,6 +11,7 @@ from django.utils.cache import (
     cc_delim_re, get_conditional_response, set_response_etag,
 )
 from django.utils.deprecation import MiddlewareMixin, RemovedInDjango21Warning
+from django.utils.http import escape_leading_slashes
 
 
 class CommonMiddleware(MiddlewareMixin):
@@ -88,6 +89,8 @@ class CommonMiddleware(MiddlewareMixin):
         POST, PUT, or PATCH.
         """
         new_path = request.get_full_path(force_append_slash=True)
+        # Prevent construction of scheme relative urls.
+        new_path = escape_leading_slashes(new_path)
         if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
             raise RuntimeError(
                 "You called this URL via %(method)s, but the URL doesn't end "
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index c5b0811..f89fc5a 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -17,7 +17,7 @@ from django.core.checks.urls import check_resolver
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.datastructures import MultiValueDict
 from django.utils.functional import cached_property
-from django.utils.http import RFC3986_SUBDELIMS
+from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
 from django.utils.regex_helper import normalize
 from django.utils.translation import get_language
 
@@ -604,9 +604,7 @@ class URLResolver:
                     # safe characters from `pchar` definition of RFC 3986
                     url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
                     # Don't allow construction of scheme relative urls.
-                    if url.startswith('//'):
-                        url = '/%%2F%s' % url[2:]
-                    return url
+                    return escape_leading_slashes(url)
         # lookup_view can be URL name or callable, but callables are not
         # friendly in error messages.
         m = getattr(lookup_view, '__module__', None)
diff --git a/django/utils/http.py b/django/utils/http.py
index 5e90050..fb51ca8 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -437,3 +437,14 @@ def limited_parse_qsl(qs, keep_blank_values=False, encoding='utf-8',
             value = unquote(value, encoding=encoding, errors=errors)
             r.append((name, value))
     return r
+
+
+def escape_leading_slashes(url):
+    """
+    If redirecting to an absolute path (two leading slashes), a slash must be
+    escaped to prevent browsers from handling the path as schemaless and
+    redirecting to another host.
+    """
+    if url.startswith('//'):
+        url = '/%2F{}'.format(url[2:])
+    return url
diff --git a/docs/releases/1.11.15.txt b/docs/releases/1.11.15.txt
index 397681d..fca551e 100644
--- a/docs/releases/1.11.15.txt
+++ b/docs/releases/1.11.15.txt
@@ -5,3 +5,16 @@ Django 1.11.15 release notes
 *August 1, 2018*
 
 Django 1.11.15 fixes a security issue in 1.11.14.
+
+CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
+=================================================================
+
+If the :class:`~django.middleware.common.CommonMiddleware` and the
+:setting:`APPEND_SLASH` setting are both enabled, and if the project has a
+URL pattern that accepts any path ending in a slash (many content management
+systems have such a pattern), then a request to a maliciously crafted URL of
+that site could lead to a redirect to another site, enabling phishing and other
+attacks.
+
+``CommonMiddleware`` now escapes leading slashes to prevent redirects to other
+domains.
diff --git a/docs/releases/2.0.8.txt b/docs/releases/2.0.8.txt
index aa47389..167d998 100644
--- a/docs/releases/2.0.8.txt
+++ b/docs/releases/2.0.8.txt
@@ -6,6 +6,19 @@ Django 2.0.8 release notes
 
 Django 2.0.8 fixes a security issue and a bug in 2.0.7.
 
+CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
+=================================================================
+
+If the :class:`~django.middleware.common.CommonMiddleware` and the
+:setting:`APPEND_SLASH` setting are both enabled, and if the project has a
+URL pattern that accepts any path ending in a slash (many content management
+systems have such a pattern), then a request to a maliciously crafted URL of
+that site could lead to a redirect to another site, enabling phishing and other
+attacks.
+
+``CommonMiddleware`` now escapes leading slashes to prevent redirects to other
+domains.
+
 Bugfixes
 ========
 
diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py
index 8006938..fcf3da2 100644
--- a/tests/middleware/tests.py
+++ b/tests/middleware/tests.py
@@ -133,6 +133,25 @@ class CommonMiddlewareTest(SimpleTestCase):
         self.assertEqual(r.status_code, 301)
         self.assertEqual(r.url, '/needsquoting%23/')
 
+    @override_settings(APPEND_SLASH=True)
+    def test_append_slash_leading_slashes(self):
+        """
+        Paths starting with two slashes are escaped to prevent open redirects.
+        If there's a URL pattern that allows paths to start with two slashes, a
+        request with path //evil.com must not redirect to //evil.com/ (appended
+        slash) which is a schemaless absolute URL. The browser would navigate
+        to evil.com/.
+        """
+        # Use 4 slashes because of RequestFactory behavior.
+        request = self.rf.get('////evil.com/security')
+        response = HttpResponseNotFound()
+        r = CommonMiddleware().process_request(request)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+        r = CommonMiddleware().process_response(request, response)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+
     @override_settings(APPEND_SLASH=False, PREPEND_WWW=True)
     def test_prepend_www(self):
         request = self.rf.get('/path/')
diff --git a/tests/middleware/urls.py b/tests/middleware/urls.py
index 8c6621d..d623e7d 100644
--- a/tests/middleware/urls.py
+++ b/tests/middleware/urls.py
@@ -6,4 +6,6 @@ urlpatterns = [
     url(r'^noslash$', views.empty_view),
     url(r'^slash/$', views.empty_view),
     url(r'^needsquoting#/$', views.empty_view),
+    # Accepts paths with two leading slashes.
+    url(r'^(.+)/security/$', views.empty_view),
 ]
diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
index 9b32520..c95133d 100644
--- a/tests/utils_tests/test_http.py
+++ b/tests/utils_tests/test_http.py
@@ -6,10 +6,10 @@ from django.utils import http
 from django.utils.datastructures import MultiValueDict
 from django.utils.deprecation import RemovedInDjango21Warning
 from django.utils.http import (
-    base36_to_int, cookie_date, http_date, int_to_base36, is_safe_url,
-    is_same_domain, parse_etags, parse_http_date, quote_etag, urlencode,
-    urlquote, urlquote_plus, urlsafe_base64_decode, urlsafe_base64_encode,
-    urlunquote, urlunquote_plus,
+    base36_to_int, cookie_date, escape_leading_slashes, http_date,
+    int_to_base36, is_safe_url, is_same_domain, parse_etags, parse_http_date,
+    quote_etag, urlencode, urlquote, urlquote_plus, urlsafe_base64_decode,
+    urlsafe_base64_encode, urlunquote, urlunquote_plus,
 )
 
 
@@ -275,3 +275,14 @@ class HttpDateProcessingTests(unittest.TestCase):
     def test_parsing_asctime(self):
         parsed = parse_http_date('Sun Nov  6 08:49:37 1994')
         self.assertEqual(datetime.utcfromtimestamp(parsed), datetime(1994, 11, 6, 8, 49, 37))
+
+
+class EscapeLeadingSlashesTests(unittest.TestCase):
+    def test(self):
+        tests = (
+            ('//example.com', '/%2Fexample.com'),
+            ('//', '/%2F'),
+        )
+        for url, expected in tests:
+            with self.subTest(url=url):
+                self.assertEqual(escape_leading_slashes(url), expected)


1.11.x.diff

commit d6eaee092709aad477a9894598496c6deec532ff
Author: Andreas Hug <andreas.hug@moccu.com>
Date:   Tue Jul 24 16:18:17 2018 -0400

    [1.11.x] Fixed CVE-2018-14574 -- Fixed open redirect possibility in CommonMiddleware.

diff --git a/django/middleware/common.py b/django/middleware/common.py
index d18d23f..fff46ba 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -11,6 +11,7 @@ from django.utils.cache import (
 )
 from django.utils.deprecation import MiddlewareMixin, RemovedInDjango21Warning
 from django.utils.encoding import force_text
+from django.utils.http import escape_leading_slashes
 from django.utils.six.moves.urllib.parse import urlparse
 
 
@@ -90,6 +91,8 @@ class CommonMiddleware(MiddlewareMixin):
         POST, PUT, or PATCH.
         """
         new_path = request.get_full_path(force_append_slash=True)
+        # Prevent construction of scheme relative urls.
+        new_path = escape_leading_slashes(new_path)
         if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
             raise RuntimeError(
                 "You called this URL via %(method)s, but the URL doesn't end "
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index 1de59a8..25e9ae8 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -20,7 +20,9 @@ from django.utils import lru_cache, six
 from django.utils.datastructures import MultiValueDict
 from django.utils.encoding import force_str, force_text
 from django.utils.functional import cached_property
-from django.utils.http import RFC3986_SUBDELIMS, urlquote
+from django.utils.http import (
+    RFC3986_SUBDELIMS, escape_leading_slashes, urlquote,
+)
 from django.utils.regex_helper import normalize
 from django.utils.translation import get_language
 
@@ -465,9 +467,7 @@ class RegexURLResolver(LocaleRegexProvider):
                     # safe characters from `pchar` definition of RFC 3986
                     url = urlquote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + str('/~:@'))
                     # Don't allow construction of scheme relative urls.
-                    if url.startswith('//'):
-                        url = '/%%2F%s' % url[2:]
-                    return url
+                    return escape_leading_slashes(url)
         # lookup_view can be URL name or callable, but callables are not
         # friendly in error messages.
         m = getattr(lookup_view, '__module__', None)
diff --git a/django/utils/http.py b/django/utils/http.py
index 1fbc11b..644d4d0 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -466,3 +466,14 @@ def limited_parse_qsl(qs, keep_blank_values=False, encoding='utf-8',
                 value = unquote(nv[1].replace(b'+', b' '))
             r.append((name, value))
     return r
+
+
+def escape_leading_slashes(url):
+    """
+    If redirecting to an absolute path (two leading slashes), a slash must be
+    escaped to prevent browsers from handling the path as schemaless and
+    redirecting to another host.
+    """
+    if url.startswith('//'):
+        url = '/%2F{}'.format(url[2:])
+    return url
diff --git a/docs/releases/1.11.15.txt b/docs/releases/1.11.15.txt
index 397681d..fca551e 100644
--- a/docs/releases/1.11.15.txt
+++ b/docs/releases/1.11.15.txt
@@ -5,3 +5,16 @@ Django 1.11.15 release notes
 *August 1, 2018*
 
 Django 1.11.15 fixes a security issue in 1.11.14.
+
+CVE-2018-14574: Open redirect possibility in ``CommonMiddleware``
+=================================================================
+
+If the :class:`~django.middleware.common.CommonMiddleware` and the
+:setting:`APPEND_SLASH` setting are both enabled, and if the project has a
+URL pattern that accepts any path ending in a slash (many content management
+systems have such a pattern), then a request to a maliciously crafted URL of
+that site could lead to a redirect to another site, enabling phishing and other
+attacks.
+
+``CommonMiddleware`` now escapes leading slashes to prevent redirects to other
+domains.
diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py
index 1d473ef..d9d701f 100644
--- a/tests/middleware/tests.py
+++ b/tests/middleware/tests.py
@@ -137,6 +137,25 @@ class CommonMiddlewareTest(SimpleTestCase):
         self.assertEqual(r.status_code, 301)
         self.assertEqual(r.url, '/needsquoting%23/')
 
+    @override_settings(APPEND_SLASH=True)
+    def test_append_slash_leading_slashes(self):
+        """
+        Paths starting with two slashes are escaped to prevent open redirects.
+        If there's a URL pattern that allows paths to start with two slashes, a
+        request with path //evil.com must not redirect to //evil.com/ (appended
+        slash) which is a schemaless absolute URL. The browser would navigate
+        to evil.com/.
+        """
+        # Use 4 slashes because of RequestFactory behavior.
+        request = self.rf.get('////evil.com/security')
+        response = HttpResponseNotFound()
+        r = CommonMiddleware().process_request(request)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+        r = CommonMiddleware().process_response(request, response)
+        self.assertEqual(r.status_code, 301)
+        self.assertEqual(r.url, '/%2Fevil.com/security/')
+
     @override_settings(APPEND_SLASH=False, PREPEND_WWW=True)
     def test_prepend_www(self):
         request = self.rf.get('/path/')
diff --git a/tests/middleware/urls.py b/tests/middleware/urls.py
index 8c6621d..d623e7d 100644
--- a/tests/middleware/urls.py
+++ b/tests/middleware/urls.py
@@ -6,4 +6,6 @@ urlpatterns = [
     url(r'^noslash$', views.empty_view),
     url(r'^slash/$', views.empty_view),
     url(r'^needsquoting#/$', views.empty_view),
+    # Accepts paths with two leading slashes.
+    url(r'^(.+)/security/$', views.empty_view),
 ]
diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
index b435e33..d339e8a 100644
--- a/tests/utils_tests/test_http.py
+++ b/tests/utils_tests/test_http.py
@@ -248,3 +248,13 @@ class HttpDateProcessingTests(unittest.TestCase):
     def test_parsing_asctime(self):
         parsed = http.parse_http_date('Sun Nov  6 08:49:37 1994')
         self.assertEqual(datetime.utcfromtimestamp(parsed), datetime(1994, 11, 6, 8, 49, 37))
+
+
+class EscapeLeadingSlashesTests(unittest.TestCase):
+    def test(self):
+        tests = (
+            ('//example.com', '/%2Fexample.com'),
+            ('//', '/%2F'),
+        )
+        for url, expected in tests:
+            self.assertEqual(http.escape_leading_slashes(url), expected)
Comment 1 Karol Babioch 2018-07-26 05:51:39 UTC
Created attachment 778044 [details]
master.diff
Comment 2 Karol Babioch 2018-07-26 05:52:00 UTC
Created attachment 778045 [details]
2.0.x.diff
Comment 3 Karol Babioch 2018-07-26 05:52:18 UTC
Created attachment 778046 [details]
2.1.x.diff
Comment 4 Karol Babioch 2018-07-26 05:52:41 UTC
Created attachment 778047 [details]
1.11.x.diff
Comment 5 Karol Babioch 2018-07-26 05:54:32 UTC
This is an embargoed bug. This means that this information is not public. Please
- do not talk to other people about this unless they're involved in fixing the issue
- do not submit this into OBS (e.g. fix Leap) until this is public
- do not make this bug public
- Please be aware that the SUSE:SLE-12-SP4:GA codestream is available via OBS. This means that you can't submit security fixes for embargoed issues to SLE 12 SP4 GA until they become public.

In doubt please talk to us on IRC (#security) or sent us a mail.

CRD: 2018-08-01 14:00 UTC
Comment 6 Karol Babioch 2018-07-27 10:13:15 UTC
By looking at the code, it seems that only the following codestream is affected:

SUSE:SLE-12-SP3:Update:Products:Cloud8:Update

The other codestreams contain older versions of python-Django that do not contain the patched code. Also they weren't mentioned in the upstream report.

However, I'm not sure whether Cloud 8 is using python-Django in the described capacity. Would be nice, if someone with more knowledge about Cloud could look into this. Since the upstream patch is available, we probably should patch it anyway, though.
Comment 7 Ondřej Súkup 2018-08-08 10:10:29 UTC
is public by 1.8.2018 - official Django release 1.11.15
Comment 8 Swamp Workflow Management 2018-08-08 10:40:06 UTC
This is an autogenerated message for OBS integration:
This bug (1102680) was mentioned in
https://build.opensuse.org/request/show/628044 Factory / python-Django1
https://build.opensuse.org/request/show/628045 15.0 / python-Django1
Comment 9 Swamp Workflow Management 2018-08-10 11:40:06 UTC
This is an autogenerated message for OBS integration:
This bug (1102680) was mentioned in
https://build.opensuse.org/request/show/628595 15.0 / python-Django
Comment 10 Swamp Workflow Management 2018-08-10 13:20:06 UTC
This is an autogenerated message for OBS integration:
This bug (1102680) was mentioned in
https://build.opensuse.org/request/show/628623 Backports:SLE-12 / python-Django
Comment 11 Swamp Workflow Management 2018-08-14 19:08:51 UTC
openSUSE-SU-2018:2327-1: An update that fixes one vulnerability is now available.

Category: security (moderate)
Bug References: 1102680
CVE References: CVE-2018-14574
Sources used:
SUSE Package Hub for SUSE Linux Enterprise 12 (src):    python-Django-1.11.15-11.1
Comment 12 Swamp Workflow Management 2018-08-16 13:22:06 UTC
openSUSE-SU-2018:2375-1: An update that fixes one vulnerability is now available.

Category: security (important)
Bug References: 1102680
CVE References: CVE-2018-14574
Sources used:
openSUSE Leap 15.0 (src):    python-Django1-1.11.15-lp150.2.3.1
Comment 13 Swamp Workflow Management 2018-08-24 10:08:37 UTC
openSUSE-SU-2018:2488-1: An update that fixes one vulnerability is now available.

Category: security (moderate)
Bug References: 1102680
CVE References: CVE-2018-14574
Sources used:
openSUSE Leap 15.0 (src):    python-Django-2.0.8-lp150.2.3.1
Comment 16 Swamp Workflow Management 2018-09-22 07:16:00 UTC
openSUSE-SU-2018:2488-2: An update that fixes one vulnerability is now available.

Category: security (moderate)
Bug References: 1102680
CVE References: CVE-2018-14574
Sources used:
openSUSE Backports SLE-15 (src):    python-Django-2.0.8-bp150.3.3.1
Comment 17 Swamp Workflow Management 2018-09-22 07:30:40 UTC
openSUSE-SU-2018:2809-1: An update that fixes one vulnerability is now available.

Category: security (important)
Bug References: 1102680
CVE References: CVE-2018-14574
Sources used:
openSUSE Backports SLE-15 (src):    python-Django1-1.11.15-bp150.3.3.1
Comment 18 Swamp Workflow Management 2018-10-29 20:08:30 UTC
SUSE-SU-2018:3549-1: An update that fixes one vulnerability is now available.

Category: security (moderate)
Bug References: 1102680
CVE References: CVE-2018-14574
Sources used:
SUSE OpenStack Cloud Crowbar 8 (src):    python-Django-1.11.11-3.3.1
SUSE OpenStack Cloud 8 (src):    python-Django-1.11.11-3.3.1
HPE Helion Openstack 8 (src):    python-Django-1.11.11-3.3.1
Comment 19 Alexandros Toptsoglou 2020-03-20 14:36:12 UTC
Done