[adding 874...@bugs.debian.org to CC] Hi Salvatore,
> > > There is as well a no-dsa tagged entry (CVE-2017-12794), which is only > > > relevant when "DEBUG = true". But as we do an update now via a DSA, we > > > can include this fix as well. > > > > That makes sense. Shall I go ahead and add this CVE-2017-12794 and send > > another debdiff? > > Yes please. Full diff attached. Please let me know if this is okay to upload. Source: python-django Version: 1:1.10.7-2+deb9u2 Distribution: stretch-security Urgency: high Maintainer: Chris Lamb <la...@debian.org> Timestamp: 1533177448 Date: Thu, 02 Aug 2018 10:37:28 +0800 Closes: 874415 905216 Changes: python-django (1:1.10.7-2+deb9u2) stretch-security; urgency=high . * Non-maintainer upload by the Security Team. * CVE-2018-14574: Fix an open redirect possibility in CommonMiddleware. If the django.middleware.common.CommonMiddleware and the APPEND_SLASH setting were both enabled, and if the project has a URL pattern that accepted any path ending in a slash then a request to a maliciously crafted URL of that site could lead to a redirect to another site, enabling phishing and other attacks. (Closes: #905216) * CVE-2017-12794: Fix a cross-site scripting attack in the technical HTTP 500 page. This vulnerability did not affect production sites as they typically do not run with "DEBUG = True". (Closes: #874415) Regards, -- ,''`. : :' : Chris Lamb `. `'` la...@debian.org / chris-lamb.co.uk `-
diff --git a/debian/changelog b/debian/changelog index 472d500fb..e77a81a21 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +python-django (1:1.10.7-2+deb9u2) stretch-security; urgency=high + + * Non-maintainer upload by the Security Team. + * CVE-2018-14574: Fix an open redirect possibility in CommonMiddleware. + If the django.middleware.common.CommonMiddleware and the APPEND_SLASH + setting were both enabled, and if the project has a URL pattern that + accepted any path ending in a slash then a request to a maliciously crafted + URL of that site could lead to a redirect to another site, enabling + phishing and other attacks. (Closes: #905216) + * CVE-2017-12794: Fix a cross-site scripting attack in the technical HTTP 500 + page. This vulnerability did not affect production sites as they typically + do not run with "DEBUG = True". (Closes: #874415) + + -- Chris Lamb <la...@debian.org> Thu, 02 Aug 2018 10:37:28 +0800 + python-django (1:1.10.7-2+deb9u1) stretch-security; urgency=high * Non-maintainer upload by the LTS Team. diff --git a/debian/patches/0015-CVE-2018-14574.patch b/debian/patches/0015-CVE-2018-14574.patch new file mode 100644 index 000000000..c8bf439e9 --- /dev/null +++ b/debian/patches/0015-CVE-2018-14574.patch @@ -0,0 +1,153 @@ +From: Chris Lamb <la...@debian.org> +Date: Thu, 2 Aug 2018 10:28:56 +0800 +Subject: 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. + +Thanks Andreas Hug for reporting this issue. + + -- <https://www.djangoproject.com/weblog/2018/aug/01/security-releases/> + +Backported by Chris Lamb <la...@debian.org> from: + + https://github.com/django/django/commit/d6eaee092709aad477a9894598496c6deec532ff +--- + django/middleware/common.py | 3 +++ + django/urls/resolvers.py | 8 ++++---- + django/utils/http.py | 11 +++++++++++ + tests/middleware/tests.py | 19 +++++++++++++++++++ + tests/middleware/urls.py | 2 ++ + tests/utils_tests/test_http.py | 10 ++++++++++ + 6 files changed, 49 insertions(+), 4 deletions(-) + +diff --git a/django/middleware/common.py b/django/middleware/common.py +index 4cec6f0..4ac5e01 100644 +--- a/django/middleware/common.py ++++ b/django/middleware/common.py +@@ -9,6 +9,7 @@ from django.urls import is_valid_path + from django.utils.cache import get_conditional_response, set_response_etag + from django.utils.deprecation import MiddlewareMixin + from django.utils.encoding import force_text ++from django.utils.http import escape_leading_slashes + from django.utils.http import unquote_etag + 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 cec960d..da82d56 100644 +--- a/django/urls/resolvers.py ++++ b/django/urls/resolvers.py +@@ -18,7 +18,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 + +@@ -373,9 +375,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 812ddb2..3898331 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(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/tests/middleware/tests.py b/tests/middleware/tests.py +index f87bb9d..0120529 100644 +--- a/tests/middleware/tests.py ++++ b/tests/middleware/tests.py +@@ -122,6 +122,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 efe6b9a..ed4a099 100644 +--- a/tests/utils_tests/test_http.py ++++ b/tests/utils_tests/test_http.py +@@ -211,3 +211,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) diff --git a/debian/patches/0016-CVE-2017-12794.patch b/debian/patches/0016-CVE-2017-12794.patch new file mode 100644 index 000000000..d472183db --- /dev/null +++ b/debian/patches/0016-CVE-2017-12794.patch @@ -0,0 +1,144 @@ +From: Chris Lamb <la...@debian.org> +Date: Fri, 3 Aug 2018 11:48:27 +0800 +Subject: CVE-2017-12794 + +Fix a cross-site scripting attack in the technical HTTP 500 +page. This vulnerability did not affect production sites as they +typically do not run with "DEBUG = True". +--- + debian/changelog | 3 +++ + django/views/debug.py | 20 +++++++++----------- + tests/view_tests/tests/py3_test_debug.py | 13 +++++++------ + 3 files changed, 19 insertions(+), 17 deletions(-) + +diff --git a/debian/changelog b/debian/changelog +index ace826d..43cc398 100644 +--- a/debian/changelog ++++ b/debian/changelog +@@ -7,6 +7,9 @@ python-django (1:1.10.7-2+deb9u2) stretch-security; urgency=high + accepted any path ending in a slash then a request to a maliciously crafted + URL of that site could lead to a redirect to another site, enabling + phishing and other attacks. (Closes: #905216) ++ * CVE-2017-12794: Fix a cross-site scripting attack in the technical HTTP 500 ++ debug page. This vulnerability did not affect production sites as they ++ typically do not run with "DEBUG = True". (Closes: #874415) + + -- Chris Lamb <la...@debian.org> Thu, 02 Aug 2018 10:37:28 +0800 + +diff --git a/django/views/debug.py b/django/views/debug.py +index 0ed55fd..327ff43 100644 +--- a/django/views/debug.py ++++ b/django/views/debug.py +@@ -775,38 +775,37 @@ TECHNICAL_500_TEMPLATE = (""" + <h2>Traceback <span class="commands">{% if not is_email %}<a href="#" onclick="return switchPastebinFriendly(this);"> + Switch to copy-and-paste view</a></span>{% endif %} + </h2> +- {% autoescape off %} + <div id="browserTraceback"> + <ul class="traceback"> + {% for frame in frames %} + {% ifchanged frame.exc_cause %}{% if frame.exc_cause %} + <li><h3> + {% if frame.exc_cause_explicit %} +- The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception: ++ The above exception ({{ frame.exc_cause|force_escape }}) was the direct cause of the following exception: + {% else %} +- During handling of the above exception ({{ frame.exc_cause }}), another exception occurred: ++ During handling of the above exception ({{ frame.exc_cause|force_escape }}), another exception occurred: + {% endif %} + </h3></li> + {% endif %}{% endifchanged %} + <li class="frame {{ frame.type }}"> +- <code>{{ frame.filename|escape }}</code> in <code>{{ frame.function|escape }}</code> ++ <code>{{ frame.filename }}</code> in <code>{{ frame.function }}</code> + + {% if frame.context_line %} + <div class="context" id="c{{ frame.id }}"> + {% if frame.pre_context and not is_email %} + <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}"> + {% for line in frame.pre_context %} +- <li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line|escape }}</pre></li> ++ <li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line }}</pre></li> + {% endfor %} + </ol> + {% endif %} + <ol start="{{ frame.lineno }}" class="context-line"> + <li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre> +-""" """{{ frame.context_line|escape }}</pre>{% if not is_email %} <span>...</span>{% endif %}</li></ol> ++""" """{{ frame.context_line }}</pre>{% if not is_email %} <span>...</span>{% endif %}</li></ol> + {% if frame.post_context and not is_email %} + <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}"> + {% for line in frame.post_context %} +- <li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line|escape }}</pre></li> ++ <li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line }}</pre></li> + {% endfor %} + </ol> + {% endif %} +@@ -831,7 +830,7 @@ TECHNICAL_500_TEMPLATE = (""" + <tbody> + {% for var in frame.vars|dictsort:0 %} + <tr> +- <td>{{ var.0|force_escape }}</td> ++ <td>{{ var.0 }}</td> + <td class="code"><pre>{{ var.1 }}</pre></td> + </tr> + {% endfor %} +@@ -842,7 +841,6 @@ TECHNICAL_500_TEMPLATE = (""" + {% endfor %} + </ul> + </div> +- {% endautoescape %} + <form action="http://dpaste.com/" name="pasteform" id="pasteform" method="post"> + {% if not is_email %} + <div id="pastebinTraceback" class="pastebin"> +@@ -888,9 +886,9 @@ In template {{ template_info.name }}, error at line {{ template_info.line }} + + Traceback:{% for frame in frames %} + {% ifchanged frame.exc_cause %}{% if frame.exc_cause %}{% if frame.exc_cause_explicit %} +-The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception: ++The above exception ({{ frame.exc_cause|force_escape }}) was the direct cause of the following exception: + {% else %} +-During handling of the above exception ({{ frame.exc_cause }}), another exception occurred: ++During handling of the above exception ({{ frame.exc_cause|force_escape }}), another exception occurred: + {% endif %}{% endif %}{% endifchanged %} + File "{{ frame.filename|escape }}" in {{ frame.function|escape }} + {% if frame.context_line %} {{ frame.lineno }}. {{ frame.context_line|escape }}{% endif %}{% endfor %} +diff --git a/tests/view_tests/tests/py3_test_debug.py b/tests/view_tests/tests/py3_test_debug.py +index 30201ba..316179a 100644 +--- a/tests/view_tests/tests/py3_test_debug.py ++++ b/tests/view_tests/tests/py3_test_debug.py +@@ -9,6 +9,7 @@ error (raise ... from ...) can't be silenced using NOQA. + import sys + + from django.test import RequestFactory, TestCase ++from django.utils.safestring import mark_safe + from django.views.debug import ExceptionReporter + + +@@ -20,10 +21,10 @@ class Py3ExceptionReporterTests(TestCase): + request = self.rf.get('/test_view/') + try: + try: +- raise AttributeError('Top level') ++ raise AttributeError(mark_safe('<p>Top level</p>')) + except AttributeError as explicit: + try: +- raise ValueError('Second exception') from explicit ++ raise ValueError('<p>Second exception</p>') from explicit + except ValueError: + raise IndexError('Final exception') + except Exception: +@@ -37,9 +38,9 @@ class Py3ExceptionReporterTests(TestCase): + html = reporter.get_traceback_html() + # Both messages are twice on page -- one rendered as html, + # one as plain text (for pastebin) +- self.assertEqual(2, html.count(explicit_exc.format("Top level"))) +- self.assertEqual(2, html.count(implicit_exc.format("Second exception"))) ++ self.assertEqual(2, html.count(explicit_exc.format('<p>Top level</p>'))) ++ self.assertEqual(2, html.count(implicit_exc.format('<p>Second exception</p>'))) + + text = reporter.get_traceback_text() +- self.assertIn(explicit_exc.format("Top level"), text) +- self.assertIn(implicit_exc.format("Second exception"), text) ++ self.assertIn(explicit_exc.format('<p>Top level</p>'), text) ++ self.assertIn(implicit_exc.format('<p>Second exception</p>'), text) diff --git a/debian/patches/series b/debian/patches/series index 13daeb8c4..0b2048f8f 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -5,3 +5,5 @@ fix-migration-fake-initial-2.patch fix-test-middleware-classes-headers.patch 0013-CVE-2018-7536.patch 0014-CVE-2018-7537.patch +0015-CVE-2018-14574.patch +0016-CVE-2017-12794.patch