[Python-modules-commits] [python-django] 01/01: New upstream security release: https://www.djangoproject.com/weblog/2015/jan/13/security/

Raphaël Hertzog hertzog at moszumanska.debian.org
Wed Jan 28 09:45:11 UTC 2015


This is an automated email from the git hooks/post-receive script.

hertzog pushed a commit to branch debian/wheezy
in repository python-django.

commit d79ef7c64879797bd46f78f5a1df61c1e8d0963a
Author: Raphaël Hertzog <hertzog at debian.org>
Date:   Wed Jan 28 10:43:31 2015 +0100

    New upstream security release: https://www.djangoproject.com/weblog/2015/jan/13/security/
    
    * New upstream security release:
      https://www.djangoproject.com/weblog/2015/jan/13/security/
      - WSGI header spoofing via underscore/dash conflation (CVE-2015-0219)
      - Possible XSS attack via user-supplied redirect URLs (CVE-2015-0220)
      - Denial-of-service attack against django.views.static.serve
        (CVE-2015-0221)
    * Also include a fix for a regression introduced by the patch for
      CVE-2015-0221: https://code.djangoproject.com/ticket/24158
---
 debian/changelog                                 |  13 +++
 debian/patches/CVE-2015-0219.diff                | 105 +++++++++++++++++
 debian/patches/CVE-2015-0220.diff                |  52 +++++++++
 debian/patches/CVE-2015-0221-regression-fix.diff | 138 +++++++++++++++++++++++
 debian/patches/CVE-2015-0221.diff                | 100 ++++++++++++++++
 debian/patches/series                            |   5 +
 6 files changed, 413 insertions(+)

diff --git a/debian/changelog b/debian/changelog
index a616732..2c59f9d 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
+python-django (1.4.5-1+deb7u8) stable-security; urgency=medium
+
+  * New upstream security release:
+    https://www.djangoproject.com/weblog/2015/jan/13/security/
+    - WSGI header spoofing via underscore/dash conflation (CVE-2015-0219)
+    - Possible XSS attack via user-supplied redirect URLs (CVE-2015-0220)
+    - Denial-of-service attack against django.views.static.serve
+      (CVE-2015-0221)
+  * Also include a fix for a regression introduced by the patch for
+    CVE-2015-0221: https://code.djangoproject.com/ticket/24158
+
+ -- Raphaël Hertzog <hertzog at debian.org>  Wed, 28 Jan 2015 10:24:59 +0100
+
 python-django (1.4.5-1+deb7u7) stable-security; urgency=high
 
   * New upstream security release.
diff --git a/debian/patches/CVE-2015-0219.diff b/debian/patches/CVE-2015-0219.diff
new file mode 100644
index 0000000..861dae5
--- /dev/null
+++ b/debian/patches/CVE-2015-0219.diff
@@ -0,0 +1,105 @@
+commit 4f6fffc1dc429f1ad428ecf8e6620739e8837450
+Author: Carl Meyer <carl at oddbird.net>
+Date:   Wed Sep 10 11:06:19 2014 -0600
+
+    [1.4.x] Stripped headers containing underscores to prevent spoofing in WSGI environ.
+    
+    This is a security fix. Disclosure following shortly.
+    
+    Thanks to Jedediah Smith for the report.
+
+diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
+index 8d4ceab..0ec5f98 100644
+--- a/django/core/servers/basehttp.py
++++ b/django/core/servers/basehttp.py
+@@ -199,6 +199,17 @@ class WSGIRequestHandler(simple_server.WSGIRequestHandler, object):
+ 
+         sys.stderr.write(msg)
+ 
++    def get_environ(self):
++        # Strip all headers with underscores in the name before constructing
++        # the WSGI environ. This prevents header-spoofing based on ambiguity
++        # between underscores and dashes both normalized to underscores in WSGI
++        # env vars. Nginx and Apache 2.4+ both do this as well.
++        for k, v in self.headers.items():
++            if '_' in k:
++                del self.headers[k]
++
++        return super(WSGIRequestHandler, self).get_environ()
++
+ 
+ class AdminMediaHandler(handlers.StaticFilesHandler):
+     """
+diff --git a/tests/regressiontests/servers/servers/test_basehttp.py b/tests/regressiontests/servers/servers/test_basehttp.py
+new file mode 100644
+index 0000000..6bca608
+--- /dev/null
++++ b/tests/regressiontests/servers/servers/test_basehttp.py
+@@ -0,0 +1,67 @@
++import sys
++
++from django.core.servers.basehttp import WSGIRequestHandler
++from django.test import TestCase
++from django.utils.six import BytesIO, StringIO
++
++
++class Stub(object):
++    def __init__(self, **kwargs):
++        self.__dict__.update(kwargs)
++
++
++class WSGIRequestHandlerTestCase(TestCase):
++
++    def test_strips_underscore_headers(self):
++        """WSGIRequestHandler ignores headers containing underscores.
++
++        This follows the lead of nginx and Apache 2.4, and is to avoid
++        ambiguity between dashes and underscores in mapping to WSGI environ,
++        which can have security implications.
++        """
++        def test_app(environ, start_response):
++            """A WSGI app that just reflects its HTTP environ."""
++            start_response('200 OK', [])
++            http_environ_items = sorted(
++                '%s:%s' % (k, v) for k, v in environ.items()
++                if k.startswith('HTTP_')
++            )
++            yield (','.join(http_environ_items)).encode('utf-8')
++
++        rfile = BytesIO()
++        rfile.write(b"GET / HTTP/1.0\r\n")
++        rfile.write(b"Some-Header: good\r\n")
++        rfile.write(b"Some_Header: bad\r\n")
++        rfile.write(b"Other_Header: bad\r\n")
++        rfile.seek(0)
++
++        # WSGIRequestHandler closes the output file; we need to make this a
++        # no-op so we can still read its contents.
++        class UnclosableBytesIO(BytesIO):
++            def close(self):
++                pass
++
++        wfile = UnclosableBytesIO()
++
++        def makefile(mode, *a, **kw):
++            if mode == 'rb':
++                return rfile
++            elif mode == 'wb':
++                return wfile
++
++        request = Stub(makefile=makefile)
++        server = Stub(base_environ={}, get_app=lambda: test_app)
++
++        # We don't need to check stderr, but we don't want it in test output
++        old_stderr = sys.stderr
++        sys.stderr = StringIO()
++        try:
++            # instantiating a handler runs the request as side effect
++            WSGIRequestHandler(request, '192.168.0.2', server)
++        finally:
++            sys.stderr = old_stderr
++
++        wfile.seek(0)
++        body = list(wfile.readlines())[-1]
++
++        self.assertEqual(body, b'HTTP_SOME_HEADER:good')
diff --git a/debian/patches/CVE-2015-0220.diff b/debian/patches/CVE-2015-0220.diff
new file mode 100644
index 0000000..b88e916
--- /dev/null
+++ b/debian/patches/CVE-2015-0220.diff
@@ -0,0 +1,52 @@
+commit 4c241f1b710da6419d9dca160e80b23b82db7758
+Author: Tim Graham <timograham at gmail.com>
+Date:   Wed Dec 3 16:14:00 2014 -0500
+
+    [1.4.x] Fixed is_safe_url() to handle leading whitespace.
+    
+    This is a security fix. Disclosure following shortly.
+
+diff --git a/django/utils/http.py b/django/utils/http.py
+index 2d40489..e69a92b 100644
+--- a/django/utils/http.py
++++ b/django/utils/http.py
+@@ -234,6 +234,7 @@ def is_safe_url(url, host=None):
+     """
+     if not url:
+         return False
++    url = url.strip()
+     # Chrome treats \ completely as /
+     url = url.replace('\\', '/')
+     # Chrome considers any URL with more than two slashes to be absolute, but
+diff --git a/tests/regressiontests/utils/http.py b/tests/regressiontests/utils/http.py
+index 802b3fa..3ec237a 100644
+--- a/tests/regressiontests/utils/http.py
++++ b/tests/regressiontests/utils/http.py
+@@ -64,7 +64,7 @@ class TestUtilsHttp(unittest.TestCase):
+         # bad input
+         for n in [-1, sys.maxint+1, '1', 'foo', {1:2}, (1,2,3)]:
+             self.assertRaises(ValueError, http.int_to_base36, n)
+-        
++
+         for n in ['#', ' ']:
+             self.assertRaises(ValueError, http.base36_to_int, n)
+ 
+@@ -73,7 +73,7 @@ class TestUtilsHttp(unittest.TestCase):
+ 
+         # non-integer input
+         self.assertRaises(TypeError, http.int_to_base36, 3.141)
+-        
++
+         # more explicit output testing
+         for n, b36 in [(0, '0'), (1, '1'), (42, '16'), (818469960, 'django')]:
+             self.assertEqual(http.int_to_base36(n), b36)
+@@ -97,7 +97,8 @@ class TestUtilsHttp(unittest.TestCase):
+                         'http:/\//example.com',
+                         'http:\/example.com',
+                         'http:/\example.com',
+-                        'javascript:alert("XSS")'):
++                        'javascript:alert("XSS")'
++                        '\njavascript:alert(x)'):
+             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',
diff --git a/debian/patches/CVE-2015-0221-regression-fix.diff b/debian/patches/CVE-2015-0221-regression-fix.diff
new file mode 100644
index 0000000..691eb8e
--- /dev/null
+++ b/debian/patches/CVE-2015-0221-regression-fix.diff
@@ -0,0 +1,138 @@
+commit 1e39d0f6280abf34c7719db5e7ed1c333f5e5919
+Author: Benjamin Richter <richter.benjamin at gmail.com>
+Date:   Sun Jan 25 23:22:46 2015 +0100
+
+    [1.4.x] Fixed #24158 -- Allowed GZipMiddleware to work with streaming responses
+    
+    Backport of django.utils.text.compress_sequence and fix for
+    django.middleware.gzip.GZipMiddleware when using iterators as
+    response.content.
+
+diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py
+index 69f938c..eb4d8bf 100644
+--- a/django/middleware/gzip.py
++++ b/django/middleware/gzip.py
+@@ -1,6 +1,6 @@
+ import re
+ 
+-from django.utils.text import compress_string
++from django.utils.text import compress_string, compress_sequence
+ from django.utils.cache import patch_vary_headers
+ 
+ re_accepts_gzip = re.compile(r'\bgzip\b')
+@@ -12,8 +12,9 @@ class GZipMiddleware(object):
+     on the Accept-Encoding header.
+     """
+     def process_response(self, request, response):
++        # The response object can tell us whether content is a string or an iterable
+         # It's not worth attempting to compress really short responses.
+-        if len(response.content) < 200:
++        if not response._base_content_is_iter and len(response.content) < 200:
+             return response
+ 
+         patch_vary_headers(response, ('Accept-Encoding',))
+@@ -32,15 +33,23 @@ class GZipMiddleware(object):
+         if not re_accepts_gzip.search(ae):
+             return response
+ 
+-        # Return the compressed content only if it's actually shorter.
+-        compressed_content = compress_string(response.content)
+-        if len(compressed_content) >= len(response.content):
+-            return response
++        # The response object can tell us whether content is a string or an iterable
++        if response._base_content_is_iter:
++            # If the response content is iterable we don't know the length, so delete the header.
++            del response['Content-Length']
++            # Wrap the response content in a streaming gzip iterator (direct access to inner response._container)
++            response.content = compress_sequence(response._container)
++        else:
++            # Return the compressed content only if it's actually shorter.
++            compressed_content = compress_string(response.content)
++            if len(compressed_content) >= len(response.content):
++                return response
++            response.content = compressed_content
++            response['Content-Length'] = str(len(response.content))
+ 
+         if response.has_header('ETag'):
+             response['ETag'] = re.sub('"$', ';gzip"', response['ETag'])
+ 
+-        response.content = compressed_content
+         response['Content-Encoding'] = 'gzip'
+-        response['Content-Length'] = str(len(response.content))
++
+         return response
+diff --git a/django/utils/text.py b/django/utils/text.py
+index eaafb96..8e43dc9 100644
+--- a/django/utils/text.py
++++ b/django/utils/text.py
+@@ -286,6 +286,39 @@ def compress_string(s):
+ 
+ ustring_re = re.compile(u"([\u0080-\uffff])")
+ 
++# Backported from django 1.5
++class StreamingBuffer(object):
++    def __init__(self):
++        self.vals = []
++
++    def write(self, val):
++        self.vals.append(val)
++
++    def read(self):
++        ret = ''.join(self.vals)
++        self.vals = []
++        return ret
++
++    def flush(self):
++        return
++
++    def close(self):
++        return
++
++# Backported from django 1.5
++# Like compress_string, but for iterators of strings.
++def compress_sequence(sequence):
++    buf = StreamingBuffer()
++    zfile = GzipFile(mode='wb', compresslevel=6, fileobj=buf)
++    # Output headers...
++    yield buf.read()
++    for item in sequence:
++        zfile.write(item)
++        zfile.flush()
++        yield buf.read()
++    zfile.close()
++    yield buf.read()
++
+ def javascript_quote(s, quote_double_quotes=False):
+ 
+     def fix(match):
+diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py
+index 138ee50..87b19fb 100644
+--- a/tests/regressiontests/middleware/tests.py
++++ b/tests/regressiontests/middleware/tests.py
+@@ -514,6 +514,7 @@ class GZipMiddlewareTest(TestCase):
+     short_string = "This string is too short to be worth compressing."
+     compressible_string = 'a' * 500
+     uncompressible_string = ''.join(chr(random.randint(0, 255)) for _ in xrange(500))
++    iterator_as_content = iter(compressible_string)
+ 
+     def setUp(self):
+         self.req = HttpRequest()
+@@ -589,6 +590,18 @@ class GZipMiddlewareTest(TestCase):
+         self.assertEqual(r.content, self.uncompressible_string)
+         self.assertEqual(r.get('Content-Encoding'), None)
+ 
++    def test_streaming_compression(self):
++        """
++        Tests that iterators as response content return a compressed stream without consuming
++        the whole response.content while doing so.
++        See #24158.
++        """
++        self.resp.content = self.iterator_as_content
++        r = GZipMiddleware().process_response(self.req, self.resp)
++        self.assertEqual(self.decompress(''.join(r.content)), self.compressible_string)
++        self.assertEqual(r.get('Content-Encoding'), 'gzip')
++        self.assertEqual(r.get('Content-Length'), None)
++
+ 
+ class ETagGZipMiddlewareTest(TestCase):
+     """
diff --git a/debian/patches/CVE-2015-0221.diff b/debian/patches/CVE-2015-0221.diff
new file mode 100644
index 0000000..cc7de41
--- /dev/null
+++ b/debian/patches/CVE-2015-0221.diff
@@ -0,0 +1,100 @@
+commit d020da6646c5142bc092247d218a3d1ce3e993f7
+Author: Tim Graham <timograham at gmail.com>
+Date:   Tue Dec 9 15:32:03 2014 -0500
+
+    [1.4.x] Prevented views.static.serve() from using large memory on large files.
+    
+    This is a security fix. Disclosure following shortly.
+
+diff --git a/django/views/static.py b/django/views/static.py
+index ed23779..7677d7b 100644
+--- a/django/views/static.py
++++ b/django/views/static.py
+@@ -16,6 +16,9 @@ from django.template import loader, Template, Context, TemplateDoesNotExist
+ from django.utils.http import http_date, parse_http_date
+ from django.utils.translation import ugettext as _, ugettext_noop
+ 
++STREAM_CHUNK_SIZE = 4096
++
++
+ def serve(request, path, document_root=None, show_indexes=False):
+     """
+     Serve static files below a given point in the directory structure.
+@@ -59,8 +62,8 @@ def serve(request, path, document_root=None, show_indexes=False):
+     if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
+                               statobj.st_mtime, statobj.st_size):
+         return HttpResponseNotModified(mimetype=mimetype)
+-    with open(fullpath, 'rb') as f:
+-        response = HttpResponse(f.read(), mimetype=mimetype)
++    f = open(fullpath, 'rb')
++    response = HttpResponse(iter(lambda: f.read(STREAM_CHUNK_SIZE), ''), mimetype=mimetype)
+     response["Last-Modified"] = http_date(statobj.st_mtime)
+     if stat.S_ISREG(statobj.st_mode):
+         response["Content-Length"] = statobj.st_size
+diff --git a/tests/regressiontests/views/media/long-line.txt b/tests/regressiontests/views/media/long-line.txt
+new file mode 100644
+index 0000000..b4e1948
+--- /dev/null
++++ b/tests/regressiontests/views/media/long-line.txt
+@@ -0,0 +1 @@
++lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua hic tempora est veritatis culpa fugiat doloribus fugit in sed harum veniam porro eveniet maxime labore assumenda non illum possimus aut vero laudantium cum magni numquam dolorem explicabo quidem quasi nesciunt ipsum deleniti facilis neque similique nisi ad magnam accusamus quae provident dolor ab atque modi laboriosam fuga suscipit ea beatae ipsam consequatur saepe d [...]
+diff --git a/tests/regressiontests/views/tests/static.py b/tests/regressiontests/views/tests/static.py
+index 3088a86..278eaf5 100644
+--- a/tests/regressiontests/views/tests/static.py
++++ b/tests/regressiontests/views/tests/static.py
+@@ -7,6 +7,7 @@ from django.conf import settings
+ from django.conf.urls.static import static
+ from django.test import TestCase
+ from django.http import HttpResponseNotModified
++from django.views.static import STREAM_CHUNK_SIZE
+ 
+ from .. import urls
+ from ..urls import media_dir
+@@ -29,10 +30,19 @@ class StaticTests(TestCase):
+         for filename in media_files:
+             response = self.client.get('/views/%s/%s' % (self.prefix, filename))
+             file_path = path.join(media_dir, filename)
+-            self.assertEqual(open(file_path).read(), response.content)
+-            self.assertEqual(len(response.content), int(response['Content-Length']))
++            content = response.content
++            self.assertEqual(open(file_path).read(), content)
++            self.assertEqual(len(content), int(response['Content-Length']))
+             self.assertEqual(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None))
+ 
++    def test_chunked(self):
++        "The static view should stream files in chunks to avoid large memory usage"
++        response = self.client.get('/views/%s/%s' % (self.prefix, 'long-line.txt'))
++        first_chunk = iter(response).next()
++        self.assertEqual(len(first_chunk), STREAM_CHUNK_SIZE)
++        second_chunk = response.next()
++        self.assertEqual(len(second_chunk), 1451)
++
+     def test_unknown_mime_type(self):
+         response = self.client.get('/views/%s/file.unknown' % self.prefix)
+         self.assertEqual('application/octet-stream', response['Content-Type'])
+@@ -71,9 +81,9 @@ class StaticTests(TestCase):
+         response = self.client.get('/views/%s/%s' % (self.prefix, file_name),
+                                    HTTP_IF_MODIFIED_SINCE=invalid_date)
+         file = open(path.join(media_dir, file_name))
+-        self.assertEqual(file.read(), response.content)
+-        self.assertEqual(len(response.content),
+-                          int(response['Content-Length']))
++        content = response.content
++        self.assertEqual(file.read(), content)
++        self.assertEqual(len(content), int(response['Content-Length']))
+ 
+     def test_invalid_if_modified_since2(self):
+         """Handle even more bogus If-Modified-Since values gracefully
+@@ -86,9 +96,9 @@ class StaticTests(TestCase):
+         response = self.client.get('/views/%s/%s' % (self.prefix, file_name),
+                                    HTTP_IF_MODIFIED_SINCE=invalid_date)
+         file = open(path.join(media_dir, file_name))
+-        self.assertEqual(file.read(), response.content)
+-        self.assertEqual(len(response.content),
+-                          int(response['Content-Length']))
++        content = response.content
++        self.assertEqual(file.read(), content)
++        self.assertEqual(len(content), int(response['Content-Length']))
+ 
+ 
+ class StaticHelperTest(StaticTests):
diff --git a/debian/patches/series b/debian/patches/series
index a9f4b5e..aa61e7f 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -13,3 +13,8 @@ mysql-typecast-1.4.x.diff
 is_safe_url_1_4.diff
 drop_fix_ie_for_vary_1_4.diff
 FTBFS-exception-in-servers-tests-tear-down.patch
+CVE-2015-0219.diff
+CVE-2015-0220.diff
+CVE-2015-0221.diff
+CVE-2015-0221-regression-fix.diff
+

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django.git



More information about the Python-modules-commits mailing list