[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



retitle 983526 buster-pu: package python-django/1:1.11.29-1~deb10u2
thanks

Hi Salvatore,

> There are as well yet open other issues (which similarly do not
> warrant a DSA), CVE-2021-3281, CVE-2020-24583 and CVE-2020-24584. 
> Can you add fixes for those as well?

Of course, that was silly of me not to check.

Please find an updated patch attached, which also adopts your
suggested version number:

   python-django (1:1.11.29-1~deb10u2) buster; urgency=medium
   .
     * CVE-2020-24583: Fix incorrect permissions on intermediate-level directories
       on Python 3.7+. FILE_UPLOAD_DIRECTORY_PERMISSIONS mode was not applied to
       intermediate-level directories created in the process of uploading files
       and to intermediate-level collected static directories when using the
       collectstatic management command. You should review and manually fix
       permissions on existing intermediate-level directories. (Closes: #969367)
   .
     * CVE-2020-24584: Correct permission escalation vulnerability in
       intermediate-level directories of the file system cache. On Python 3.7 and
       above, the intermediate-level directories of the file system cache had the
       system's standard umask rather than 0o077 (no group or others permissions).
       (Closes: #969367)
   .
     * CVE-2021-3281: Fix a potential directory-traversal exploit via
       archive.extract(). The django.utils.archive.extract() function, used by
       startapp --template and startproject --template, allowed directory
       traversal via an archive with absolute paths or relative paths with dot
       segments. (Closes: #981562)
   .
     * 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)

The tests all pass for me. Do note that this adds python-pytest as a
test-time dependency (a backport of sorts of Python 3.x's "pathlib"
module).


Regards,

-- 
      ,''`.
     : :'  :     Chris Lamb
     `. `'`      lamby@debian.org 🍥 chris-lamb.co.uk
       `-
diff --git a/debian/changelog b/debian/changelog
index 00bbc0532..12cc85c31 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,32 @@
+python-django (1:1.11.29-1~deb10u2) buster; urgency=medium
+
+  * CVE-2020-24583: Fix incorrect permissions on intermediate-level directories
+    on Python 3.7+. FILE_UPLOAD_DIRECTORY_PERMISSIONS mode was not applied to
+    intermediate-level directories created in the process of uploading files
+    and to intermediate-level collected static directories when using the
+    collectstatic management command. You should review and manually fix
+    permissions on existing intermediate-level directories. (Closes: #969367)
+
+  * CVE-2020-24584: Correct permission escalation vulnerability in
+    intermediate-level directories of the file system cache. On Python 3.7 and
+    above, the intermediate-level directories of the file system cache had the
+    system's standard umask rather than 0o077 (no group or others permissions).
+    (Closes: #969367)
+
+  * CVE-2021-3281: Fix a potential directory-traversal exploit via
+    archive.extract(). The django.utils.archive.extract() function, used by
+    startapp --template and startproject --template, allowed directory
+    traversal via an archive with absolute paths or relative paths with dot
+    segments. (Closes: #981562)
+
+  * 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)
+
+ -- Chris Lamb <lamby@debian.org>  Fri, 26 Feb 2021 10:07:49 +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/control b/debian/control
index f1a8bd6b7..51a16f7e3 100644
--- a/debian/control
+++ b/debian/control
@@ -22,6 +22,7 @@ Build-Depends:
  python-jinja2 <!nocheck>,
  python-mock <!nocheck>,
  python-numpy <!nocheck>,
+ python-pathlib <!nocheck>,
  python-pil <!nocheck>,
  python-psycopg2-doc <!nodoc>,
  python-setuptools,
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/0011-CVE-2020-24583.patch b/debian/patches/0011-CVE-2020-24583.patch
new file mode 100644
index 000000000..0733fc22f
--- /dev/null
+++ b/debian/patches/0011-CVE-2020-24583.patch
@@ -0,0 +1,158 @@
+From: Chris Lamb <lamby@debian.org>
+Date: Fri, 26 Feb 2021 09:46:08 +0000
+Subject: CVE-2020-24583
+
+---
+ django/core/files/storage.py                       |  6 +--
+ tests/file_storage/tests.py                        | 16 ++++---
+ .../project/documents/nested/css/base.css          |  1 +
+ tests/staticfiles_tests/test_storage.py            | 49 +++++++++++++++-------
+ 4 files changed, 48 insertions(+), 24 deletions(-)
+ create mode 100644 tests/staticfiles_tests/project/documents/nested/css/base.css
+
+diff --git a/django/core/files/storage.py b/django/core/files/storage.py
+index 98c89dd..9643198 100644
+--- a/django/core/files/storage.py
++++ b/django/core/files/storage.py
+@@ -310,9 +310,9 @@ class FileSystemStorage(Storage):
+         if not os.path.exists(directory):
+             try:
+                 if self.directory_permissions_mode is not None:
+-                    # os.makedirs applies the global umask, so we reset it,
+-                    # for consistency with file_permissions_mode behavior.
+-                    old_umask = os.umask(0)
++                    # Set the umask because os.makedirs() doesn't apply the "mode"
++                    # argument to intermediate-level directories.
++                    old_umask = os.umask(0o777 & ~self.directory_permissions_mode)
+                     try:
+                         os.makedirs(directory, self.directory_permissions_mode)
+                     finally:
+diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
+index 3ac6483..7537f60 100644
+--- a/tests/file_storage/tests.py
++++ b/tests/file_storage/tests.py
+@@ -10,6 +10,7 @@ import threading
+ import time
+ import unittest
+ from datetime import datetime, timedelta
++from pathlib import Path
+ 
+ from django.core.cache import cache
+ from django.core.exceptions import SuspiciousFileOperation, SuspiciousOperation
+@@ -928,16 +929,19 @@ class FileStoragePermissions(unittest.TestCase):
+     @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=0o765)
+     def test_file_upload_directory_permissions(self):
+         self.storage = FileSystemStorage(self.storage_dir)
+-        name = self.storage.save("the_directory/the_file", ContentFile("data"))
+-        dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777
+-        self.assertEqual(dir_mode, 0o765)
++        name = self.storage.save('the_directory/subdir/the_file', ContentFile('data'))
++        file_path = Path(self.storage.path(name))
++        self.assertEqual(file_path.parent.stat().st_mode & 0o777, 0o765)
++        self.assertEqual(file_path.parent.parent.stat().st_mode & 0o777, 0o765)
+ 
+     @override_settings(FILE_UPLOAD_DIRECTORY_PERMISSIONS=None)
+     def test_file_upload_directory_default_permissions(self):
+         self.storage = FileSystemStorage(self.storage_dir)
+-        name = self.storage.save("the_directory/the_file", ContentFile("data"))
+-        dir_mode = os.stat(os.path.dirname(self.storage.path(name)))[0] & 0o777
+-        self.assertEqual(dir_mode, 0o777 & ~self.umask)
++        name = self.storage.save('the_directory/subdir/the_file', ContentFile('data'))
++        file_path = Path(self.storage.path(name))
++        expected_mode = 0o777 & ~self.umask
++        self.assertEqual(file_path.parent.stat().st_mode & 0o777, expected_mode)
++        self.assertEqual(file_path.parent.parent.stat().st_mode & 0o777, expected_mode)
+ 
+ 
+ class FileStoragePathParsing(SimpleTestCase):
+diff --git a/tests/staticfiles_tests/project/documents/nested/css/base.css b/tests/staticfiles_tests/project/documents/nested/css/base.css
+new file mode 100644
+index 0000000..06041ca
+--- /dev/null
++++ b/tests/staticfiles_tests/project/documents/nested/css/base.css
+@@ -0,0 +1 @@
++html {height: 100%;}
+diff --git a/tests/staticfiles_tests/test_storage.py b/tests/staticfiles_tests/test_storage.py
+index d0dcafc..4065901 100644
+--- a/tests/staticfiles_tests/test_storage.py
++++ b/tests/staticfiles_tests/test_storage.py
+@@ -5,6 +5,7 @@ import shutil
+ import sys
+ import tempfile
+ import unittest
++from pathlib import Path
+ 
+ from django.conf import settings
+ from django.contrib.staticfiles import finders, storage
+@@ -509,12 +510,18 @@ class TestStaticFilePermissions(CollectionTestCase):
+     )
+     def test_collect_static_files_permissions(self):
+         call_command('collectstatic', **self.command_params)
+-        test_file = os.path.join(settings.STATIC_ROOT, "test.txt")
+-        test_dir = os.path.join(settings.STATIC_ROOT, "subdir")
+-        file_mode = os.stat(test_file)[0] & 0o777
+-        dir_mode = os.stat(test_dir)[0] & 0o777
++        static_root = Path(settings.STATIC_ROOT)
++        test_file = static_root / 'test.txt'
++        file_mode = test_file.stat().st_mode & 0o777
+         self.assertEqual(file_mode, 0o655)
+-        self.assertEqual(dir_mode, 0o765)
++        tests = [
++            static_root / 'subdir',
++            static_root / 'nested',
++            static_root / 'nested' / 'css',
++        ]
++        for directory in tests:
++            dir_mode = directory.stat().st_mode & 0o777
++            self.assertEqual(dir_mode, 0o765)
+ 
+     @override_settings(
+         FILE_UPLOAD_PERMISSIONS=None,
+@@ -522,12 +529,18 @@ class TestStaticFilePermissions(CollectionTestCase):
+     )
+     def test_collect_static_files_default_permissions(self):
+         call_command('collectstatic', **self.command_params)
+-        test_file = os.path.join(settings.STATIC_ROOT, "test.txt")
+-        test_dir = os.path.join(settings.STATIC_ROOT, "subdir")
+-        file_mode = os.stat(test_file)[0] & 0o777
+-        dir_mode = os.stat(test_dir)[0] & 0o777
++        static_root = Path(settings.STATIC_ROOT)
++        test_file = static_root / 'test.txt'
++        file_mode = test_file.stat().st_mode & 0o777
+         self.assertEqual(file_mode, 0o666 & ~self.umask)
+-        self.assertEqual(dir_mode, 0o777 & ~self.umask)
++        tests = [
++            static_root / 'subdir',
++            static_root / 'nested',
++            static_root / 'nested' / 'css',
++        ]
++        for directory in tests:
++            dir_mode = directory.stat().st_mode & 0o777
++            self.assertEqual(dir_mode, 0o777 & ~self.umask)
+ 
+     @override_settings(
+         FILE_UPLOAD_PERMISSIONS=0o655,
+@@ -536,12 +549,18 @@ class TestStaticFilePermissions(CollectionTestCase):
+     )
+     def test_collect_static_files_subclass_of_static_storage(self):
+         call_command('collectstatic', **self.command_params)
+-        test_file = os.path.join(settings.STATIC_ROOT, "test.txt")
+-        test_dir = os.path.join(settings.STATIC_ROOT, "subdir")
+-        file_mode = os.stat(test_file)[0] & 0o777
+-        dir_mode = os.stat(test_dir)[0] & 0o777
++        static_root = Path(settings.STATIC_ROOT)
++        test_file = static_root / 'test.txt'
++        file_mode = test_file.stat().st_mode & 0o777
+         self.assertEqual(file_mode, 0o640)
+-        self.assertEqual(dir_mode, 0o740)
++        tests = [
++            static_root / 'subdir',
++            static_root / 'nested',
++            static_root / 'nested' / 'css',
++        ]
++        for directory in tests:
++            dir_mode = directory.stat().st_mode & 0o777
++            self.assertEqual(dir_mode, 0o740)
+ 
+ 
+ @override_settings(
diff --git a/debian/patches/0012-CVE-2020-24584.patch b/debian/patches/0012-CVE-2020-24584.patch
new file mode 100644
index 000000000..841b061ce
--- /dev/null
+++ b/debian/patches/0012-CVE-2020-24584.patch
@@ -0,0 +1,75 @@
+From: Chris Lamb <lamby@debian.org>
+Date: Fri, 26 Feb 2021 10:53:17 +0000
+Subject: CVE-2020-24584
+
+---
+ django/core/cache/backends/filebased.py |  5 +++++
+ tests/cache/tests.py                    | 20 ++++++++++++++++++++
+ 2 files changed, 25 insertions(+)
+
+diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py
+index 7c2c5c7..d2e3155 100644
+--- a/django/core/cache/backends/filebased.py
++++ b/django/core/cache/backends/filebased.py
+@@ -102,6 +102,9 @@ class FileBasedCache(BaseCache):
+ 
+     def _createdir(self):
+         if not os.path.exists(self._dir):
++            # Set the umask because os.makedirs() doesn't apply the "mode" argument
++            # to intermediate-level directories.
++            old_umask = os.umask(0o077)
+             try:
+                 os.makedirs(self._dir, 0o700)
+             except OSError as e:
+@@ -109,6 +112,8 @@ class FileBasedCache(BaseCache):
+                     raise EnvironmentError(
+                         "Cache directory '%s' does not exist "
+                         "and could not be created'" % self._dir)
++            finally:
++                os.umask(old_umask)
+ 
+     def _key_to_file(self, key, version=None):
+         """
+diff --git a/tests/cache/tests.py b/tests/cache/tests.py
+index 0305020..3eea570 100644
+--- a/tests/cache/tests.py
++++ b/tests/cache/tests.py
+@@ -9,11 +9,13 @@ import io
+ import os
+ import re
+ import shutil
++import sys
+ import tempfile
+ import threading
+ import time
+ import unittest
+ import warnings
++from pathlib import Path
+ 
+ from django.conf import settings
+ from django.core import management, signals
+@@ -1391,6 +1393,24 @@ class FileBasedCacheTests(BaseCacheTests, TestCase):
+             with self.assertRaises(IOError):
+                 cache.get('foo')
+ 
++    @unittest.skipIf(sys.platform == 'win32', 'Python on Windows has a limited os.chmod().')
++    def test_cache_dir_permissions(self):
++        os.rmdir(self.dirname)
++        dir_path = Path(self.dirname) / 'nested' / 'filebasedcache'
++        for cache_params in settings.CACHES.values():
++            cache_params['LOCATION'] = str(dir_path)
++        setting_changed.send(self.__class__, setting='CACHES', enter=False)
++        cache.set('foo', 'bar')
++        self.assertIs(dir_path.exists(), True)
++        tests = [
++            dir_path,
++            dir_path.parent,
++            dir_path.parent.parent,
++        ]
++        for directory in tests:
++            dir_mode = directory.stat().st_mode & 0o777
++            self.assertEqual(dir_mode, 0o700)
++
+ 
+ @override_settings(CACHES={
+     'default': {
diff --git a/debian/patches/0013-CVE-2021-3281.patch b/debian/patches/0013-CVE-2021-3281.patch
new file mode 100644
index 000000000..0e6cd42dc
--- /dev/null
+++ b/debian/patches/0013-CVE-2021-3281.patch
@@ -0,0 +1,149 @@
+From: Chris Lamb <lamby@debian.org>
+Date: Fri, 26 Feb 2021 10:45:04 +0000
+Subject: CVE-2021-3281
+
+---
+ django/utils/archive.py                            |  16 +++++++++++---
+ tests/utils_tests/test_archive.py                  |  24 +++++++++++++++++++++
+ tests/utils_tests/traversal_archives/traversal.tar | Bin 0 -> 10240 bytes
+ .../traversal_archives/traversal_absolute.tar      | Bin 0 -> 10240 bytes
+ .../traversal_archives/traversal_disk_win.tar      | Bin 0 -> 10240 bytes
+ .../traversal_archives/traversal_disk_win.zip      | Bin 0 -> 312 bytes
+ 6 files changed, 37 insertions(+), 3 deletions(-)
+ create mode 100644 tests/utils_tests/traversal_archives/traversal.tar
+ create mode 100644 tests/utils_tests/traversal_archives/traversal_absolute.tar
+ create mode 100644 tests/utils_tests/traversal_archives/traversal_disk_win.tar
+ create mode 100644 tests/utils_tests/traversal_archives/traversal_disk_win.zip
+
+diff --git a/django/utils/archive.py b/django/utils/archive.py
+index 57e8765..22a4a5b 100644
+--- a/django/utils/archive.py
++++ b/django/utils/archive.py
+@@ -27,6 +27,7 @@ import stat
+ import tarfile
+ import zipfile
+ 
++from django.core.exceptions import SuspiciousOperation
+ from django.utils import six
+ 
+ 
+@@ -135,6 +136,13 @@ class BaseArchive(object):
+                 return False
+         return True
+ 
++    def target_filename(self, to_path, name):
++        target_path = os.path.abspath(to_path)
++        filename = os.path.abspath(os.path.join(target_path, name))
++        if not filename.startswith(target_path):
++            raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
++        return filename
++
+     def extract(self):
+         raise NotImplementedError('subclasses of BaseArchive must provide an extract() method')
+ 
+@@ -157,7 +165,7 @@ class TarArchive(BaseArchive):
+             name = member.name
+             if leading:
+                 name = self.split_leading_dir(name)[1]
+-            filename = os.path.join(to_path, name)
++            filename = self.target_filename(to_path, name)
+             if member.isdir():
+                 if filename and not os.path.exists(filename):
+                     os.makedirs(filename)
+@@ -200,11 +208,13 @@ class ZipArchive(BaseArchive):
+             info = self._archive.getinfo(name)
+             if leading:
+                 name = self.split_leading_dir(name)[1]
+-            filename = os.path.join(to_path, name)
++            if not name:
++                continue
++            filename = self.target_filename(to_path, name)
+             dirname = os.path.dirname(filename)
+             if dirname and not os.path.exists(dirname):
+                 os.makedirs(dirname)
+-            if filename.endswith(('/', '\\')):
++            if name.endswith(('/', '\\')):
+                 # A directory
+                 if not os.path.exists(filename):
+                     os.makedirs(filename)
+diff --git a/tests/utils_tests/test_archive.py b/tests/utils_tests/test_archive.py
+index b207a12..d324914 100644
+--- a/tests/utils_tests/test_archive.py
++++ b/tests/utils_tests/test_archive.py
+@@ -5,6 +5,8 @@ import sys
+ import tempfile
+ import unittest
+ 
++from django.core.exceptions import SuspiciousOperation
++from django.test import SimpleTestCase
+ from django.utils._os import upath
+ from django.utils.archive import Archive, extract
+ 
+@@ -88,3 +90,25 @@ class TestGzipTar(ArchiveTester, unittest.TestCase):
+ 
+ class TestBzip2Tar(ArchiveTester, unittest.TestCase):
+     archive = 'foobar.tar.bz2'
++
++
++class TestArchiveInvalid(SimpleTestCase):
++    def test_extract_function_traversal(self):
++        archives_dir = os.path.join(os.path.dirname(__file__), 'traversal_archives')
++        tests = [
++            ('traversal.tar', '..'),
++            ('traversal_absolute.tar', '/tmp/evil.py'),
++        ]
++        if sys.platform == 'win32':
++            tests += [
++                ('traversal_disk_win.tar', 'd:evil.py'),
++                ('traversal_disk_win.zip', 'd:evil.py'),
++            ]
++        msg = "Archive contains invalid path: '%s'"
++        for entry, invalid_path in tests:
++            try:
++                tmpdir = tempfile.mkdtemp()
++                with self.assertRaisesMessage(SuspiciousOperation, msg % invalid_path):
++                    extract(os.path.join(archives_dir, entry), tmpdir)
++            finally:
++                shutil.rmtree(tmpdir)
+diff --git a/tests/utils_tests/traversal_archives/traversal.tar b/tests/utils_tests/traversal_archives/traversal.tar
+new file mode 100644
+index 0000000..07eede5
+--- /dev/null
++++ b/tests/utils_tests/traversal_archives/traversal.tar
+@@ -0,0 +1 @@
++foo/../0000775000175000017500000000000014002521171011427 5ustar  felixxfelixxfoo/../evil.py0000664000175000017500000000000014002521171012726 0ustar  felixxfelixx
+\ No newline at end of file
+diff --git a/tests/utils_tests/traversal_archives/traversal_absolute.tar b/tests/utils_tests/traversal_archives/traversal_absolute.tar
+new file mode 100644
+index 0000000..231566b
+--- /dev/null
++++ b/tests/utils_tests/traversal_archives/traversal_absolute.tar
+@@ -0,0 +1 @@
++foo/0000775000175000017500000000000014002526407011223 5ustar  felixxfelixx/tmp/evil.py0000664000175000017500000000000014002526443012616 0ustar  felixxfelixx
+\ No newline at end of file
+diff --git a/tests/utils_tests/traversal_archives/traversal_disk_win.tar b/tests/utils_tests/traversal_archives/traversal_disk_win.tar
+new file mode 100644
+index 0000000..97f0b95
+--- /dev/null
++++ b/tests/utils_tests/traversal_archives/traversal_disk_win.tar
+@@ -0,0 +1 @@
++foo/0000775000175000017500000000000014002515370011220 5ustar  felixxfelixxfoo/d:evil.py0000664000175000017500000000000014002515370012755 0ustar  felixxfelixx
+\ No newline at end of file
+diff --git a/tests/utils_tests/traversal_archives/traversal_disk_win.zip b/tests/utils_tests/traversal_archives/traversal_disk_win.zip
+new file mode 100644
+index 0000000..e5ab208
+--- /dev/null
++++ b/tests/utils_tests/traversal_archives/traversal_disk_win.zip
+@@ -0,0 +1,11 @@
++PK
++j\6Rfoo/UT	gª
++`kª
++`uxèèPK
++j\6R
foo/d:evil.pyUT	gª
++`gª
++`uxèèPK
++j\6RýAfoo/UTgª
++`uxèèPK
++j\6R
´?>foo/d:evil.pyUTgª
++`uxèèPK??
+\ No newline at end of file
diff --git a/debian/patches/series b/debian/patches/series
index 296032c78..21b85fdc1 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -6,3 +6,7 @@
 0007-Fixed-29182-Adjusted-SQLite-schema-table-alteration-.patch
 0008-CVE-2020-13254.patch
 0009-CVE-2020-13596.patch
+0010-CVE-2021-23336.patch
+0011-CVE-2020-24583.patch
+0012-CVE-2020-24584.patch
+0013-CVE-2021-3281.patch

Reply to: