[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#983526: buster-pu: package python-django/1:1.11.29-1+deb10u1



Package: release.debian.org
Severity: normal
Tags: buster
User: release.debian.org@packages.debian.org
Usertags: pu

Dear stable release managers,

Please consider python-django (1:1.11.29-1+deb10u1) for buster:
  
  python-django (1:1.11.29-1+deb10u1) buster; urgency=high
  .
    * CVE-2021-23336: Prevent a web cache poisoning attack via "parameter
      cloaking". Django contains a copy of urllib.parse.parse_qsl() which was
      added to backport some security fixes. A further security fix has been
      issued recently such that parse_qsl() no longer allows using ";" as a
      query parameter separator by default. (Closes: #983090)
  .
      For more information, please see:
  .
        https://www.djangoproject.com/weblog/2021/feb/19/security-releases/

The full diff is attached. The security team believe this should go
via s-p-u rather than via a DLA (if at all):

   https://bugs.debian.org/983090#27

Please double-check the version number for me. The current version in
buster-security is 1:1.11.29-1~deb10u1 (with a tilde).


Regards,

-- 
      ,''`.
     : :'  :     Chris Lamb
     `. `'`      lamby@debian.org / chris-lamb.co.uk
       `-
diff --git a/debian/changelog b/debian/changelog
index 00bbc0532..2683a597b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,17 @@
+python-django (1:1.11.29-1+deb10u1) buster; urgency=high
+
+  * CVE-2021-23336: Prevent a web cache poisoning attack via "parameter
+    cloaking". Django contains a copy of urllib.parse.parse_qsl() which was
+    added to backport some security fixes. A further security fix has been
+    issued recently such that parse_qsl() no longer allows using ";" as a
+    query parameter separator by default. (Closes: #983090)
+
+    For more information, please see:
+
+      https://www.djangoproject.com/weblog/2021/feb/19/security-releases/
+
+ -- Chris Lamb <lamby@debian.org>  Thu, 25 Feb 2021 16:37:19 +0000
+
 python-django (1:1.11.29-1~deb10u1) buster-security; urgency=high
 
   * New upstream security release (postponed from March 2020):
diff --git a/debian/patches/0010-CVE-2021-23336.patch b/debian/patches/0010-CVE-2021-23336.patch
new file mode 100644
index 000000000..192ac7251
--- /dev/null
+++ b/debian/patches/0010-CVE-2021-23336.patch
@@ -0,0 +1,147 @@
+From: Chris Lamb <lamby@debian.org>
+Date: Thu, 25 Feb 2021 16:27:58 +0000
+Subject: CVE-2021-23336
+
+---
+ django/utils/http.py                        |  2 +-
+ tests/handlers/test_exception.py            |  2 +-
+ tests/requests/test_data_upload_settings.py |  8 ++---
+ tests/utils_tests/test_http.py              | 55 +++++++++++++++++++++++++++++
+ 4 files changed, 61 insertions(+), 6 deletions(-)
+
+diff --git a/django/utils/http.py b/django/utils/http.py
+index 644d4d0..adeabe9 100644
+--- a/django/utils/http.py
++++ b/django/utils/http.py
+@@ -56,7 +56,7 @@ ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
+ RFC3986_GENDELIMS = str(":/?#[]@")
+ RFC3986_SUBDELIMS = str("!$&'()*+,;=")
+ 
+-FIELDS_MATCH = re.compile('[&;]')
++FIELDS_MATCH = re.compile('&')
+ 
+ 
+ @keep_lazy_text
+diff --git a/tests/handlers/test_exception.py b/tests/handlers/test_exception.py
+index 7afd4ac..0c1e763 100644
+--- a/tests/handlers/test_exception.py
++++ b/tests/handlers/test_exception.py
+@@ -6,7 +6,7 @@ from django.test.client import FakePayload
+ class ExceptionHandlerTests(SimpleTestCase):
+ 
+     def get_suspicious_environ(self):
+-        payload = FakePayload('a=1&a=2;a=3\r\n')
++        payload = FakePayload('a=1&a=2&a=3\r\n')
+         return {
+             'REQUEST_METHOD': 'POST',
+             'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+diff --git a/tests/requests/test_data_upload_settings.py b/tests/requests/test_data_upload_settings.py
+index f60f185..44897cc 100644
+--- a/tests/requests/test_data_upload_settings.py
++++ b/tests/requests/test_data_upload_settings.py
+@@ -11,7 +11,7 @@ TOO_MUCH_DATA_MSG = 'Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE.
+ 
+ class DataUploadMaxMemorySizeFormPostTests(SimpleTestCase):
+     def setUp(self):
+-        payload = FakePayload('a=1&a=2;a=3\r\n')
++        payload = FakePayload('a=1&a=2&a=3\r\n')
+         self.request = WSGIRequest({
+             'REQUEST_METHOD': 'POST',
+             'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+@@ -117,7 +117,7 @@ class DataUploadMaxNumberOfFieldsGet(SimpleTestCase):
+                 request = WSGIRequest({
+                     'REQUEST_METHOD': 'GET',
+                     'wsgi.input': BytesIO(b''),
+-                    'QUERY_STRING': 'a=1&a=2;a=3',
++                    'QUERY_STRING': 'a=1&a=2&a=3',
+                 })
+                 request.GET['a']
+ 
+@@ -126,7 +126,7 @@ class DataUploadMaxNumberOfFieldsGet(SimpleTestCase):
+             request = WSGIRequest({
+                 'REQUEST_METHOD': 'GET',
+                 'wsgi.input': BytesIO(b''),
+-                'QUERY_STRING': 'a=1&a=2;a=3',
++                'QUERY_STRING': 'a=1&a=2&a=3',
+             })
+             request.GET['a']
+ 
+@@ -168,7 +168,7 @@ class DataUploadMaxNumberOfFieldsMultipartPost(SimpleTestCase):
+ 
+ class DataUploadMaxNumberOfFieldsFormPost(SimpleTestCase):
+     def setUp(self):
+-        payload = FakePayload("\r\n".join(['a=1&a=2;a=3', '']))
++        payload = FakePayload("\r\n".join(['a=1&a=2&a=3', '']))
+         self.request = WSGIRequest({
+             'REQUEST_METHOD': 'POST',
+             'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
+index d339e8a..b1184c2 100644
+--- a/tests/utils_tests/test_http.py
++++ b/tests/utils_tests/test_http.py
+@@ -5,6 +5,7 @@ import sys
+ import unittest
+ from datetime import datetime
+ 
++from django.core.exceptions import TooManyFieldsSent
+ from django.test import ignore_warnings
+ from django.utils import http, six
+ from django.utils.datastructures import MultiValueDict
+@@ -258,3 +259,57 @@ class EscapeLeadingSlashesTests(unittest.TestCase):
+         )
+         for url, expected in tests:
+             self.assertEqual(http.escape_leading_slashes(url), expected)
++
++
++# Backport of unit tests for urllib.parse.parse_qsl() from Python 3.8.8.
++# Copyright (C) 2021 Python Software Foundation (see LICENSE.python).
++class ParseQSLBackportTests(unittest.TestCase):
++    def test_parse_qsl(self):
++        tests = [
++            ('', []),
++            ('&', []),
++            ('&&', []),
++            ('=', [('', '')]),
++            ('=a', [('', 'a')]),
++            ('a', [('a', '')]),
++            ('a=', [('a', '')]),
++            ('&a=b', [('a', 'b')]),
++            ('a=a+b&b=b+c', [('a', 'a b'), ('b', 'b c')]),
++            ('a=1&a=2', [('a', '1'), ('a', '2')]),
++            (';a=b', [(';a', 'b')]),
++            ('a=a+b;b=b+c', [('a', 'a b;b=b c')]),
++        ]
++        for original, expected in tests:
++            result = http.limited_parse_qsl(original, keep_blank_values=True)
++            self.assertEqual(result, expected, 'Error parsing %r' % original)
++            expect_without_blanks = [v for v in expected if len(v[1])]
++            result = http.limited_parse_qsl(original, keep_blank_values=False)
++            self.assertEqual(result, expect_without_blanks, 'Error parsing %r' % original)
++
++    def test_parse_qsl_encoding(self):
++        result = http.limited_parse_qsl('key=\u0141%E9', encoding='latin-1')
++        self.assertEqual(result, [('key', '\u0141\xE9')])
++
++        if sys.version_info[0] == 2:
++            result = http.limited_parse_qsl('key=\u0141%C3%A9', encoding='utf-8')
++            self.assertEqual(result, [('key', '\u0141\xc3\xa9')])
++            result = http.limited_parse_qsl('key=\u0141%C3%A9', encoding='ascii')
++            self.assertEqual(result, [('key', '\u0141\xc3\xa9')])
++            result = http.limited_parse_qsl('key=\u0141%E9-', encoding='ascii')
++            self.assertEqual(result, [('key', '\u0141\xe9-')])
++            result = http.limited_parse_qsl('key=\u0141%E9-', encoding='ascii', errors='ignore')
++            self.assertEqual(result, [('key', '\u0141\xe9-')])
++        else:
++            result = http.limited_parse_qsl('key=\u0141%C3%A9', encoding='utf-8')
++            self.assertEqual(result, [('key', '\u0141\xE9')])
++            result = http.limited_parse_qsl('key=\u0141%C3%A9', encoding='ascii')
++            self.assertEqual(result, [('key', '\u0141\ufffd\ufffd')])
++            result = http.limited_parse_qsl('key=\u0141%E9-', encoding='ascii')
++            self.assertEqual(result, [('key', '\u0141\ufffd-')])
++            result = http.limited_parse_qsl('key=\u0141%E9-', encoding='ascii', errors='ignore')
++            self.assertEqual(result, [('key', '\u0141-')])
++
++    def test_parse_qsl_field_limit(self):
++        with self.assertRaises(TooManyFieldsSent):
++            http.limited_parse_qsl('&'.join(['a=a'] * 11), fields_limit=10)
++        http.limited_parse_qsl('&'.join(['a=a'] * 10), fields_limit=10)
diff --git a/debian/patches/series b/debian/patches/series
index 296032c78..5e4af2ec9 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -6,3 +6,4 @@
 0007-Fixed-29182-Adjusted-SQLite-schema-table-alteration-.patch
 0008-CVE-2020-13254.patch
 0009-CVE-2020-13596.patch
+0010-CVE-2021-23336.patch

Reply to: