Version in base suite: 2.2.28-1~deb11u1
Base version: python-django_2.2.28-1~deb11u1
Target version: python-django_2.2.28-1~deb11u2
Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-django/python-django_2.2.28-1~deb11u1.dsc
Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-django/python-django_2.2.28-1~deb11u2.dsc
changelog | 41 +++
gitlab-ci.yml | 11 +
patches/0007-fix-url-validator.patch | 51 ++++
patches/CVE-2023-23969.patch | 90 ++++++++
patches/CVE-2023-24580.patch | 381 +++++++++++++++++++++++++++++++++++
patches/CVE-2023-31047.patch | 256 +++++++++++++++++++++++
patches/CVE-2023-36053.patch | 202 ++++++++++++++++++
patches/series | 5
8 files changed, 1037 insertions(+)
diff -Nru python-django-2.2.28/debian/changelog python-django-2.2.28/debian/changelog
--- python-django-2.2.28/debian/changelog 2022-10-14 17:02:41.000000000 +0000
+++ python-django-2.2.28/debian/changelog 2023-07-28 13:19:58.000000000 +0000
@@ -1,3 +1,44 @@
+python-django (2:2.2.28-1~deb11u2) bullseye-security; urgency=high
+
+ * CVE-2023-23969: Potential denial-of-service via Accept-Language headers.
+
+ The parsed values of Accept-Language headers are cached in order to avoid
+ repetitive parsing. This leads to a potential denial-of-service vector via
+ excessive memory usage if large header values are sent.
+
+ In order to avoid this vulnerability, the Accept-Language header is now
+ parsed up to a maximum length. (Closes: #1030251)
+
+ * CVE-2023-36053: Potential regular expression denial of service
+ vulnerability in EmailValidator/URLValidator.
+
+ EmailValidator and URLValidator were subject to potential regular
+ expression denial of service attack via a very large number of domain name
+ labels of emails and URLs. (Closes: #1040225)
+
+ * CVE-2023-31047: Prevent a potential bypass of validation when uploading
+ multiple files using one form field.
+
+ Uploading multiple files using one form field has never been supported by
+ forms.FileField or forms.ImageField as only the last uploaded file was
+ validated. Unfortunately, Uploading multiple files topic suggested
+ otherwise. In order to avoid the vulnerability, the ClearableFileInput and
+ FileInput form widgets now raise ValueError when the multiple HTML
+ attribute is set on them. To prevent the exception and keep the old
+ behavior, set the allow_multiple_selected attribute to True.
+ (Closes: #1035467)
+
+ * CVE-2023-24580: Potential denial-of-service vulnerability in file uploads
+
+ Passing certain inputs to multipart forms could result in too many open
+ files or memory exhaustion, and provided a potential vector for a
+ denial-of-service attack. The number of files parts parsed is now limited
+ via the new DATA_UPLOAD_MAX_NUMBER_FILES setting. (Closes: #1031290)
+
+ * Add/apply the URLValidator patch from sid.
+
+ -- Chris Lamb Fri, 28 Jul 2023 14:19:58 +0100
+
python-django (2:2.2.28-1~deb11u1) bullseye-security; urgency=medium
* New upstream security release:
diff -Nru python-django-2.2.28/debian/gitlab-ci.yml python-django-2.2.28/debian/gitlab-ci.yml
--- python-django-2.2.28/debian/gitlab-ci.yml 1970-01-01 00:00:00.000000000 +0000
+++ python-django-2.2.28/debian/gitlab-ci.yml 2023-07-28 13:19:58.000000000 +0000
@@ -0,0 +1,11 @@
+include:
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml
+
+variables:
+ RELEASE: bullseye
+
+lintian:
+ allow_failure: true
+
+reprotest:
+ allow_failure: true
diff -Nru python-django-2.2.28/debian/patches/0007-fix-url-validator.patch python-django-2.2.28/debian/patches/0007-fix-url-validator.patch
--- python-django-2.2.28/debian/patches/0007-fix-url-validator.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-2.2.28/debian/patches/0007-fix-url-validator.patch 2023-07-28 13:19:58.000000000 +0000
@@ -0,0 +1,51 @@
+From: Pedro Schlickmann Mendes
+Date: Thu, 20 Jul 2023 12:59:59 +0100
+Subject: Fixed URLValidator crash in some edge cases
+
+Origin: upstream, https://github.com/django/django/commit/e8b4feddc34ffe5759ec21da8fa027e86e653f1c
+Bug: https://code.djangoproject.com/ticket/33367
+Last-Update: 2021-12-15
+---
+ django/core/validators.py | 13 +++++++------
+ 1 file changed, 7 insertions(+), 6 deletions(-)
+
+--- python-django.orig/django/core/validators.py
++++ python-django/django/core/validators.py
+@@ -118,14 +118,15 @@ class URLValidator(RegexValidator):
+
+ # Then check full URL
+ try:
++ splitted_url = urlsplit(value)
++ except ValueError:
++ raise ValidationError(self.message, code=self.code, params={'value': value})
++ try:
+ super().__call__(value)
+ except ValidationError as e:
+ # Trivial case failed. Try for possible IDN domain
+ if value:
+- try:
+- scheme, netloc, path, query, fragment = urlsplit(value)
+- except ValueError: # for example, "Invalid IPv6 URL"
+- raise ValidationError(self.message, code=self.code)
++ scheme, netloc, path, query, fragment = splitted_url
+ try:
+ netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
+ except UnicodeError: # invalid domain part
+@@ -136,7 +137,7 @@ class URLValidator(RegexValidator):
+ raise
+ else:
+ # Now verify IPv6 in the netloc part
+- host_match = re.search(r'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).netloc)
++ host_match = re.search(r'^\[(.+)\](?::\d{2,5})?$', splitted_url.netloc)
+ if host_match:
+ potential_ip = host_match.groups()[0]
+ try:
+@@ -148,7 +149,7 @@ class URLValidator(RegexValidator):
+ # section 3.1. It's defined to be 255 bytes or less, but this includes
+ # one byte for the length of the name and one byte for the trailing dot
+ # that's used to indicate absolute names in DNS.
+- if len(urlsplit(value).netloc) > 253:
++ if splitted_url.hostname is None or len(splitted_url.hostname) > 253:
+ raise ValidationError(self.message, code=self.code)
+
+
diff -Nru python-django-2.2.28/debian/patches/CVE-2023-23969.patch python-django-2.2.28/debian/patches/CVE-2023-23969.patch
--- python-django-2.2.28/debian/patches/CVE-2023-23969.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-2.2.28/debian/patches/CVE-2023-23969.patch 2023-07-28 13:19:58.000000000 +0000
@@ -0,0 +1,90 @@
+From: Nick Pope
+Date: Wed, 25 Jan 2023 12:21:48 +0100
+Subject: [PATCH] [3.2.x] Fixed CVE-2023-23969 -- Prevented DoS with
+ pathological values for Accept-Language.
+
+The parsed values of Accept-Language headers are cached in order to
+avoid repetitive parsing. This leads to a potential denial-of-service
+vector via excessive memory usage if the raw value of Accept-Language
+headers is very large.
+
+Accept-Language headers are now limited to a maximum length in order
+to avoid this issue.
+---
+ django/utils/translation/trans_real.py | 32 +++++++++++++++++++++++++++++++-
+ tests/i18n/tests.py | 8 ++++++++
+ 2 files changed, 39 insertions(+), 1 deletion(-)
+
+diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
+index 486b2b26b608..f080e51b8310 100644
+--- a/django/utils/translation/trans_real.py
++++ b/django/utils/translation/trans_real.py
+@@ -29,6 +29,11 @@ _default = None
+ # magic gettext number to separate context from message
+ CONTEXT_SEPARATOR = "\x04"
+
++# Maximum number of characters that will be parsed from the Accept-Language
++# header to prevent possible denial of service or memory exhaustion attacks.
++# About 10x longer than the longest value shown on MDN's Accept-Language page.
++ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500
++
+ # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
+ # and RFC 3066, section 2.1
+ accept_language_re = re.compile(r'''
+@@ -560,7 +565,7 @@ def get_language_from_request(request, check_path=False):
+
+
+ @functools.lru_cache(maxsize=1000)
+-def parse_accept_lang_header(lang_string):
++def _parse_accept_lang_header(lang_string):
+ """
+ Parse the lang_string, which is the body of an HTTP Accept-Language
+ header, and return a tuple of (lang, q-value), ordered by 'q' values.
+@@ -582,3 +587,28 @@ def parse_accept_lang_header(lang_string):
+ result.append((lang, priority))
+ result.sort(key=lambda k: k[1], reverse=True)
+ return tuple(result)
++
++
++def parse_accept_lang_header(lang_string):
++ """
++ Parse the value of the Accept-Language header up to a maximum length.
++
++ The value of the header is truncated to a maximum length to avoid potential
++ denial of service and memory exhaustion attacks. Excessive memory could be
++ used if the raw value is very large as it would be cached due to the use of
++ functools.lru_cache() to avoid repetitive parsing of common header values.
++ """
++ # If the header value doesn't exceed the maximum allowed length, parse it.
++ if len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH:
++ return _parse_accept_lang_header(lang_string)
++
++ # If there is at least one comma in the value, parse up to the last comma
++ # before the max length, skipping any truncated parts at the end of the
++ # header value.
++ index = lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)
++ if index > 0:
++ return _parse_accept_lang_header(lang_string[:index])
++
++ # Don't attempt to parse if there is only one language-range value which is
++ # longer than the maximum allowed length and so truncated.
++ return []
+diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
+index 7381cb9c3154..7f70b15504bf 100644
+--- a/tests/i18n/tests.py
++++ b/tests/i18n/tests.py
+@@ -1282,6 +1282,14 @@ class MiscTests(SimpleTestCase):
+ ('de;q=0.', [('de', 0.0)]),
+ ('en; q=1,', [('en', 1.0)]),
+ ('en; q=1.0, * ; q=0.5', [('en', 1.0), ('*', 0.5)]),
++ (
++ 'en' + '-x' * 20,
++ [('en-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x', 1.0)],
++ ),
++ (
++ ', '.join(['en; q=1.0'] * 20),
++ [('en', 1.0)] * 20,
++ ),
+ # Bad headers
+ ('en-gb;q=1.0000', []),
+ ('en;q=0.1234', []),
diff -Nru python-django-2.2.28/debian/patches/CVE-2023-24580.patch python-django-2.2.28/debian/patches/CVE-2023-24580.patch
--- python-django-2.2.28/debian/patches/CVE-2023-24580.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-2.2.28/debian/patches/CVE-2023-24580.patch 2023-07-28 13:19:58.000000000 +0000
@@ -0,0 +1,381 @@
+From: Markus Holtermann
+Date: Tue, 13 Dec 2022 10:27:39 +0100
+Subject: [PATCH] Fixed CVE-2023-24580 -- Prevented DoS with too many uploaded
+ files.
+
+Thanks to Jakob Ackermann for the report.
+---
+ django/conf/global_settings.py | 4 ++
+ django/core/exceptions.py | 9 +++++
+ django/core/handlers/exception.py | 3 +-
+ django/http/multipartparser.py | 62 ++++++++++++++++++++++++-----
+ django/http/request.py | 6 ++-
+ docs/ref/settings.txt | 23 +++++++++++
+ tests/handlers/test_exception.py | 31 ++++++++++++++-
+ tests/requests/test_data_upload_settings.py | 53 +++++++++++++++++++++++-
+ 8 files changed, 175 insertions(+), 16 deletions(-)
+
+diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
+index f3abfada25f2..7f4cd545c21f 100644
+--- a/django/conf/global_settings.py
++++ b/django/conf/global_settings.py
+@@ -299,6 +299,10 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
+ # SuspiciousOperation (TooManyFieldsSent) is raised.
+ DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
+
++# Maximum number of files encoded in a multipart upload that will be read
++# before a SuspiciousOperation (TooManyFilesSent) is raised.
++DATA_UPLOAD_MAX_NUMBER_FILES = 100
++
+ # Directory in which upload streamed files will be temporarily saved. A value of
+ # `None` will make Django use the operating system's default temporary directory
+ # (i.e. "/tmp" on *nix systems).
+diff --git a/django/core/exceptions.py b/django/core/exceptions.py
+index 0e85397b9c74..eb5562924d20 100644
+--- a/django/core/exceptions.py
++++ b/django/core/exceptions.py
+@@ -55,6 +55,15 @@ class TooManyFieldsSent(SuspiciousOperation):
+ pass
+
+
++class TooManyFilesSent(SuspiciousOperation):
++ """
++ The number of fields in a GET or POST request exceeded
++ settings.DATA_UPLOAD_MAX_NUMBER_FILES.
++ """
++
++ pass
++
++
+ class RequestDataTooBig(SuspiciousOperation):
+ """
+ The size of the request (excluding any file uploads) exceeded
+diff --git a/django/core/handlers/exception.py b/django/core/handlers/exception.py
+index 66443ce56015..4eea18baaec4 100644
+--- a/django/core/handlers/exception.py
++++ b/django/core/handlers/exception.py
+@@ -7,6 +7,7 @@ from django.core import signals
+ from django.core.exceptions import (
+ PermissionDenied, RequestDataTooBig, SuspiciousOperation,
+ TooManyFieldsSent,
++ TooManyFilesSent,
+ )
+ from django.http import Http404
+ from django.http.multipartparser import MultiPartParserError
+@@ -64,7 +65,7 @@ def response_for_exception(request, exc):
+ )
+
+ elif isinstance(exc, SuspiciousOperation):
+- if isinstance(exc, (RequestDataTooBig, TooManyFieldsSent)):
++ if isinstance(exc, (RequestDataTooBig, TooManyFieldsSent, TooManyFilesSent)):
+ # POST data can't be accessed again, otherwise the original
+ # exception would be raised.
+ request._mark_post_parse_error()
+diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py
+index 259128acefce..a879b4893ac3 100644
+--- a/django/http/multipartparser.py
++++ b/django/http/multipartparser.py
+@@ -13,6 +13,7 @@ from urllib.parse import unquote
+ from django.conf import settings
+ from django.core.exceptions import (
+ RequestDataTooBig, SuspiciousMultipartForm, TooManyFieldsSent,
++ TooManyFilesSent,
+ )
+ from django.core.files.uploadhandler import (
+ SkipFile, StopFutureHandlers, StopUpload,
+@@ -37,6 +38,7 @@ class InputStreamExhausted(Exception):
+ RAW = "raw"
+ FILE = "file"
+ FIELD = "field"
++FIELD_TYPES = frozenset([FIELD, RAW])
+
+
+ class MultiPartParser:
+@@ -98,6 +100,22 @@ class MultiPartParser:
+ self._upload_handlers = upload_handlers
+
+ def parse(self):
++ # Call the actual parse routine and close all open files in case of
++ # errors. This is needed because if exceptions are thrown the
++ # MultiPartParser will not be garbage collected immediately and
++ # resources would be kept alive. This is only needed for errors because
++ # the Request object closes all uploaded files at the end of the
++ # request.
++ try:
++ return self._parse()
++ except Exception:
++ if hasattr(self, "_files"):
++ for _, files in self._files.lists():
++ for fileobj in files:
++ fileobj.close()
++ raise
++
++ def _parse(self):
+ """
+ Parse the POST data and break it into a FILES MultiValueDict and a POST
+ MultiValueDict.
+@@ -143,6 +161,8 @@ class MultiPartParser:
+ num_bytes_read = 0
+ # To count the number of keys in the request.
+ num_post_keys = 0
++ # To count the number of files in the request.
++ num_files = 0
+ # To limit the amount of data read from the request.
+ read_size = None
+
+@@ -155,6 +175,20 @@ class MultiPartParser:
+ self.handle_file_complete(old_field_name, counters)
+ old_field_name = None
+
++ if (
++ item_type in FIELD_TYPES
++ and settings.DATA_UPLOAD_MAX_NUMBER_FIELDS is not None
++ ):
++ # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FIELDS.
++ num_post_keys += 1
++ # 2 accounts for empty raw fields before and after the
++ # last boundary.
++ if settings.DATA_UPLOAD_MAX_NUMBER_FIELDS + 2 < num_post_keys:
++ raise TooManyFieldsSent(
++ "The number of GET/POST parameters exceeded "
++ "settings.DATA_UPLOAD_MAX_NUMBER_FIELDS."
++ )
++
+ try:
+ disposition = meta_data['content-disposition'][1]
+ field_name = disposition['name'].strip()
+@@ -167,15 +201,6 @@ class MultiPartParser:
+ field_name = force_text(field_name, encoding, errors='replace')
+
+ if item_type == FIELD:
+- # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FIELDS.
+- num_post_keys += 1
+- if (settings.DATA_UPLOAD_MAX_NUMBER_FIELDS is not None and
+- settings.DATA_UPLOAD_MAX_NUMBER_FIELDS < num_post_keys):
+- raise TooManyFieldsSent(
+- 'The number of GET/POST parameters exceeded '
+- 'settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.'
+- )
+-
+ # Avoid reading more than DATA_UPLOAD_MAX_MEMORY_SIZE.
+ if settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None:
+ read_size = settings.DATA_UPLOAD_MAX_MEMORY_SIZE - num_bytes_read
+@@ -201,6 +226,16 @@ class MultiPartParser:
+
+ self._post.appendlist(field_name, force_text(data, encoding, errors='replace'))
+ elif item_type == FILE:
++ # Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FILES.
++ num_files += 1
++ if (
++ settings.DATA_UPLOAD_MAX_NUMBER_FILES is not None
++ and num_files > settings.DATA_UPLOAD_MAX_NUMBER_FILES
++ ):
++ raise TooManyFilesSent(
++ "The number of files exceeded "
++ "settings.DATA_UPLOAD_MAX_NUMBER_FILES."
++ )
+ # This is a file, use the handler...
+ file_name = disposition.get('filename')
+ if file_name:
+@@ -268,8 +303,13 @@ class MultiPartParser:
+ # Handle file upload completions on next iteration.
+ old_field_name = field_name
+ else:
+- # If this is neither a FIELD or a FILE, just exhaust the stream.
+- exhaust(stream)
++ # If this is neither a FIELD nor a FILE, exhaust the field
++ # stream. Note: There could be an error here at some point,
++ # but there will be at least two RAW types (before and
++ # after the other boundaries). This branch is usually not
++ # reached at all, because a missing content-disposition
++ # header will skip the whole boundary.
++ exhaust(field_stream)
+ except StopUpload as e:
+ self._close_files()
+ if not e.connection_reset:
+diff --git a/django/http/request.py b/django/http/request.py
+index c93b4d478218..cff375a29704 100644
+--- a/django/http/request.py
++++ b/django/http/request.py
+@@ -11,7 +11,9 @@ from django.core.exceptions import (
+ DisallowedHost, ImproperlyConfigured, RequestDataTooBig,
+ )
+ from django.core.files import uploadhandler
+-from django.http.multipartparser import MultiPartParser, MultiPartParserError
++from django.http.multipartparser import (
++ MultiPartParser, MultiPartParserError, TooManyFilesSent,
++)
+ from django.utils.datastructures import (
+ CaseInsensitiveMapping, ImmutableList, MultiValueDict,
+ )
+@@ -313,7 +315,7 @@ class HttpRequest:
+ data = self
+ try:
+ self._post, self._files = self.parse_file_upload(self.META, data)
+- except MultiPartParserError:
++ except (MultiPartParserError, TooManyFilesSent):
+ # An error occurred while parsing POST data. Since when
+ # formatting the error the request handler might access
+ # self.POST, set self._post and self._file to prevent
+diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
+index daca1bea5690..8f562c97ee21 100644
+--- a/docs/ref/settings.txt
++++ b/docs/ref/settings.txt
+@@ -1012,6 +1012,28 @@ could be used as a denial-of-service attack vector if left unchecked. Since web
+ servers don't typically perform deep request inspection, it's not possible to
+ perform a similar check at that level.
+
++.. setting:: DATA_UPLOAD_MAX_NUMBER_FILES
++
++``DATA_UPLOAD_MAX_NUMBER_FILES``
++--------------------------------
++
++.. versionadded:: 3.2.18
++
++Default: ``100``
++
++The maximum number of files that may be received via POST in a
++``multipart/form-data`` encoded request before a
++:exc:`~django.core.exceptions.SuspiciousOperation` (``TooManyFiles``) is
++raised. You can set this to ``None`` to disable the check. Applications that
++are expected to receive an unusually large number of file fields should tune
++this setting.
++
++The number of accepted files is correlated to the amount of time and memory
++needed to process the request. Large requests could be used as a
++denial-of-service attack vector if left unchecked. Since web servers don't
++typically perform deep request inspection, it's not possible to perform a
++similar check at that level.
++
+ .. setting:: DATABASE_ROUTERS
+
+ ``DATABASE_ROUTERS``
+@@ -3449,6 +3471,7 @@ HTTP
+ ----
+ * :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE`
+ * :setting:`DATA_UPLOAD_MAX_NUMBER_FIELDS`
++* :setting:`DATA_UPLOAD_MAX_NUMBER_FILES`
+ * :setting:`DEFAULT_CHARSET`
+ * :setting:`DEFAULT_CONTENT_TYPE`
+ * :setting:`DISALLOWED_USER_AGENTS`
+diff --git a/tests/handlers/test_exception.py b/tests/handlers/test_exception.py
+index 0c1e76399045..cf1e51750afd 100644
+--- a/tests/handlers/test_exception.py
++++ b/tests/handlers/test_exception.py
+@@ -1,6 +1,11 @@
+ from django.core.handlers.wsgi import WSGIHandler
+ from django.test import SimpleTestCase, override_settings
+-from django.test.client import FakePayload
++from django.test.client import (
++ BOUNDARY,
++ MULTIPART_CONTENT,
++ FakePayload,
++ encode_multipart,
++)
+
+
+ class ExceptionHandlerTests(SimpleTestCase):
+@@ -25,3 +30,27 @@ class ExceptionHandlerTests(SimpleTestCase):
+ def test_data_upload_max_number_fields_exceeded(self):
+ response = WSGIHandler()(self.get_suspicious_environ(), lambda *a, **k: None)
+ self.assertEqual(response.status_code, 400)
++
++ @override_settings(DATA_UPLOAD_MAX_NUMBER_FILES=2)
++ def test_data_upload_max_number_files_exceeded(self):
++ payload = FakePayload(
++ encode_multipart(
++ BOUNDARY,
++ {
++ "a.txt": "Hello World!",
++ "b.txt": "Hello Django!",
++ "c.txt": "Hello Python!",
++ },
++ )
++ )
++ environ = {
++ "REQUEST_METHOD": "POST",
++ "CONTENT_TYPE": MULTIPART_CONTENT,
++ "CONTENT_LENGTH": len(payload),
++ "wsgi.input": payload,
++ "SERVER_NAME": "test",
++ "SERVER_PORT": "8000",
++ }
++
++ response = WSGIHandler()(environ, lambda *a, **k: None)
++ self.assertEqual(response.status_code, 400)
+diff --git a/tests/requests/test_data_upload_settings.py b/tests/requests/test_data_upload_settings.py
+index 44897cc9fa97..fae22cac3fa4 100644
+--- a/tests/requests/test_data_upload_settings.py
++++ b/tests/requests/test_data_upload_settings.py
+@@ -1,12 +1,17 @@
+ from io import BytesIO
+
+-from django.core.exceptions import RequestDataTooBig, TooManyFieldsSent
++from django.core.exceptions import (
++ RequestDataTooBig,
++ TooManyFieldsSent,
++ TooManyFilesSent,
++)
+ from django.core.handlers.wsgi import WSGIRequest
+ from django.test import SimpleTestCase
+ from django.test.client import FakePayload
+
+ TOO_MANY_FIELDS_MSG = 'The number of GET/POST parameters exceeded settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.'
+ TOO_MUCH_DATA_MSG = 'Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE.'
++TOO_MANY_FILES_MSG = 'The number of files exceeded settings.DATA_UPLOAD_MAX_NUMBER_FILES.'
+
+
+ class DataUploadMaxMemorySizeFormPostTests(SimpleTestCase):
+@@ -166,6 +171,52 @@ class DataUploadMaxNumberOfFieldsMultipartPost(SimpleTestCase):
+ self.request._load_post_and_files()
+
+
++class DataUploadMaxNumberOfFilesMultipartPost(SimpleTestCase):
++ def setUp(self):
++ payload = FakePayload(
++ "\r\n".join(
++ [
++ "--boundary",
++ (
++ 'Content-Disposition: form-data; name="name1"; '
++ 'filename="name1.txt"'
++ ),
++ "",
++ "value1",
++ "--boundary",
++ (
++ 'Content-Disposition: form-data; name="name2"; '
++ 'filename="name2.txt"'
++ ),
++ "",
++ "value2",
++ "--boundary--",
++ ]
++ )
++ )
++ self.request = WSGIRequest(
++ {
++ "REQUEST_METHOD": "POST",
++ "CONTENT_TYPE": "multipart/form-data; boundary=boundary",
++ "CONTENT_LENGTH": len(payload),
++ "wsgi.input": payload,
++ }
++ )
++
++ def test_number_exceeded(self):
++ with self.settings(DATA_UPLOAD_MAX_NUMBER_FILES=1):
++ with self.assertRaisesMessage(TooManyFilesSent, TOO_MANY_FILES_MSG):
++ self.request._load_post_and_files()
++
++ def test_number_not_exceeded(self):
++ with self.settings(DATA_UPLOAD_MAX_NUMBER_FILES=2):
++ self.request._load_post_and_files()
++
++ def test_no_limit(self):
++ with self.settings(DATA_UPLOAD_MAX_NUMBER_FILES=None):
++ self.request._load_post_and_files()
++
++
+ class DataUploadMaxNumberOfFieldsFormPost(SimpleTestCase):
+ def setUp(self):
+ payload = FakePayload("\r\n".join(['a=1&a=2&a=3', '']))
diff -Nru python-django-2.2.28/debian/patches/CVE-2023-31047.patch python-django-2.2.28/debian/patches/CVE-2023-31047.patch
--- python-django-2.2.28/debian/patches/CVE-2023-31047.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-2.2.28/debian/patches/CVE-2023-31047.patch 2023-07-28 13:19:58.000000000 +0000
@@ -0,0 +1,256 @@
+From: Mariusz Felisiak
+Date: Thu, 13 Apr 2023 10:10:56 +0200
+Subject: [PATCH] [3.2.x] Fixed CVE-2023-31047,
+ Fixed #31710 -- Prevented potential bypass of validation when uploading
+ multiple files using one form field.
+
+Thanks Moataz Al-Sharida and nawaik for reports.
+
+Co-authored-by: Shai Berger
+Co-authored-by: nessita <124304+nessita@users.noreply.github.com>
+---
+ django/forms/widgets.py | 26 ++++++++-
+ docs/topics/http/file-uploads.txt | 24 +++++++-
+ tests/forms_tests/field_tests/test_filefield.py | 68 +++++++++++++++++++++-
+ .../widget_tests/test_clearablefileinput.py | 5 ++
+ tests/forms_tests/widget_tests/test_fileinput.py | 43 ++++++++++++++
+ 5 files changed, 161 insertions(+), 5 deletions(-)
+
+diff --git a/django/forms/widgets.py b/django/forms/widgets.py
+index e37036c0c991..a39c76fa9d56 100644
+--- a/django/forms/widgets.py
++++ b/django/forms/widgets.py
+@@ -373,16 +373,40 @@ class MultipleHiddenInput(HiddenInput):
+
+ class FileInput(Input):
+ input_type = 'file'
++ allow_multiple_selected = False
+ needs_multipart_form = True
+ template_name = 'django/forms/widgets/file.html'
+
++ def __init__(self, attrs=None):
++ if (
++ attrs is not None and
++ not self.allow_multiple_selected and
++ attrs.get("multiple", False)
++ ):
++ raise ValueError(
++ "%s doesn't support uploading multiple files."
++ % self.__class__.__name__
++ )
++ if self.allow_multiple_selected:
++ if attrs is None:
++ attrs = {"multiple": True}
++ else:
++ attrs.setdefault("multiple", True)
++ super(FileInput, self).__init__(attrs)
++
+ def format_value(self, value):
+ """File input never renders a value."""
+ return
+
+ def value_from_datadict(self, data, files, name):
+ "File widgets take data from FILES, not POST"
+- return files.get(name)
++ getter = files.get
++ if self.allow_multiple_selected:
++ try:
++ getter = files.getlist
++ except AttributeError:
++ pass
++ return getter(name)
+
+ def value_omitted_from_data(self, data, files, name):
+ return name not in files
+diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt
+index 21a6f06853ef..1c660eb0fd4c 100644
+--- a/docs/topics/http/file-uploads.txt
++++ b/docs/topics/http/file-uploads.txt
+@@ -159,14 +159,32 @@ uploads:
+ def post(self, request, *args, **kwargs):
+ form_class = self.get_form_class()
+ form = self.get_form(form_class)
+- files = request.FILES.getlist('file_field')
+ if form.is_valid():
+- for f in files:
+- ... # Do something with each file.
+ return self.form_valid(form)
+ else:
+ return self.form_invalid(form)
+
++ def form_valid(self, form):
++ files = form.cleaned_data["file_field"]
++ for f in files:
++ ... # Do something with each file.
++ return super(FileFieldView, self).form_valid()
++
++.. warning::
++
++ This will allow you to handle multiple files at the form level only. Be
++ aware that you cannot use it to put multiple files on a single model
++ instance (in a single field), for example, even if the custom widget is used
++ with a form field related to a model ``FileField``.
++
++.. versionchanged:: 1.11.29-1+deb10u8
++
++ In previous versions, there was no support for the ``allow_multiple_selected``
++ class attribute, and users were advised to create the widget with the HTML
++ attribute ``multiple`` set through the ``attrs`` argument. However, this
++ caused validation of the form field to be applied only to the last file
++ submitted, which could have adverse security implications.
++
+ Upload Handlers
+ ===============
+
+diff --git a/tests/forms_tests/field_tests/test_filefield.py b/tests/forms_tests/field_tests/test_filefield.py
+index 33574446f4cb..8227da60e6f1 100644
+--- a/tests/forms_tests/field_tests/test_filefield.py
++++ b/tests/forms_tests/field_tests/test_filefield.py
+@@ -1,7 +1,8 @@
+ import pickle
+
+ from django.core.files.uploadedfile import SimpleUploadedFile
+-from django.forms import FileField, ValidationError
++from django.core.validators import validate_image_file_extension
++from django.forms import FileField, FileInput, ValidationError
+ from django.test import SimpleTestCase
+
+
+@@ -82,3 +83,68 @@ class FileFieldTest(SimpleTestCase):
+
+ def test_file_picklable(self):
+ self.assertIsInstance(pickle.loads(pickle.dumps(FileField())), FileField)
++
++
++class MultipleFileInput(FileInput):
++ allow_multiple_selected = True
++
++
++class MultipleFileField(FileField):
++ def __init__(self, *args, **kwargs):
++ kwargs.setdefault("widget", MultipleFileInput())
++ super(MultipleFileField, self).__init__(*args, **kwargs)
++
++ def clean(self, data, initial=None):
++ single_file_clean = super(MultipleFileField, self).clean
++ if isinstance(data, (list, tuple)):
++ result = [single_file_clean(d, initial) for d in data]
++ else:
++ result = single_file_clean(data, initial)
++ return result
++
++
++class MultipleFileFieldTest(SimpleTestCase):
++ def test_file_multiple(self):
++ f = MultipleFileField()
++ files = [
++ SimpleUploadedFile("name1", b"Content 1"),
++ SimpleUploadedFile("name2", b"Content 2"),
++ ]
++ self.assertEqual(f.clean(files), files)
++
++ def test_file_multiple_empty(self):
++ f = MultipleFileField()
++ files = [
++ SimpleUploadedFile("empty", b""),
++ SimpleUploadedFile("nonempty", b"Some Content"),
++ ]
++ msg = "'The submitted file is empty.'"
++ with self.assertRaisesMessage(ValidationError, msg):
++ f.clean(files)
++ with self.assertRaisesMessage(ValidationError, msg):
++ f.clean(files[::-1])
++
++ def test_file_multiple_validation(self):
++ f = MultipleFileField(validators=[validate_image_file_extension])
++
++ good_files = [
++ SimpleUploadedFile("image1.jpg", b"fake JPEG"),
++ SimpleUploadedFile("image2.png", b"faux image"),
++ SimpleUploadedFile("image3.bmp", b"fraudulent bitmap"),
++ ]
++ self.assertEqual(f.clean(good_files), good_files)
++
++ evil_files = [
++ SimpleUploadedFile("image1.sh", b"#!/bin/bash -c 'echo pwned!'\n"),
++ SimpleUploadedFile("image2.png", b"faux image"),
++ SimpleUploadedFile("image3.jpg", b"fake JPEG"),
++ ]
++
++ evil_rotations = (
++ evil_files[i:] + evil_files[:i] # Rotate by i.
++ for i in range(len(evil_files))
++ )
++ msg = "File extension 'sh' is not allowed. Allowed extensions are: "
++ for rotated_evil_files in evil_rotations:
++ with self.assertRaisesMessage(ValidationError, msg):
++ f.clean(rotated_evil_files)
+diff --git a/tests/forms_tests/widget_tests/test_clearablefileinput.py b/tests/forms_tests/widget_tests/test_clearablefileinput.py
+index 2ba376db8a9f..8d9e38af3ccb 100644
+--- a/tests/forms_tests/widget_tests/test_clearablefileinput.py
++++ b/tests/forms_tests/widget_tests/test_clearablefileinput.py
+@@ -161,3 +161,8 @@ class ClearableFileInputTest(WidgetTest):
+ self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True)
+ self.assertIs(widget.value_omitted_from_data({}, {'field': 'x'}, 'field'), False)
+ self.assertIs(widget.value_omitted_from_data({'field-clear': 'y'}, {}, 'field'), False)
++
++ def test_multiple_error(self):
++ msg = "ClearableFileInput doesn't support uploading multiple files."
++ with self.assertRaisesMessage(ValueError, msg):
++ ClearableFileInput(attrs={"multiple": True})
+diff --git a/tests/forms_tests/widget_tests/test_fileinput.py b/tests/forms_tests/widget_tests/test_fileinput.py
+index bbd7c7fe527c..eb6051fdb0fc 100644
+--- a/tests/forms_tests/widget_tests/test_fileinput.py
++++ b/tests/forms_tests/widget_tests/test_fileinput.py
+@@ -1,4 +1,6 @@
++from django.core.files.uploadedfile import SimpleUploadedFile
+ from django.forms import FileInput
++from django.utils.datastructures import MultiValueDict
+
+ from .base import WidgetTest
+
+@@ -18,3 +20,44 @@ class FileInputTest(WidgetTest):
+ def test_value_omitted_from_data(self):
+ self.assertIs(self.widget.value_omitted_from_data({}, {}, 'field'), True)
+ self.assertIs(self.widget.value_omitted_from_data({}, {'field': 'value'}, 'field'), False)
++
++ def test_multiple_error(self):
++ msg = "FileInput doesn't support uploading multiple files."
++ with self.assertRaisesMessage(ValueError, msg):
++ FileInput(attrs={"multiple": True})
++
++ def test_value_from_datadict_multiple(self):
++ class MultipleFileInput(FileInput):
++ allow_multiple_selected = True
++
++ file_1 = SimpleUploadedFile("something1.txt", b"content 1")
++ file_2 = SimpleUploadedFile("something2.txt", b"content 2")
++ # Uploading multiple files is allowed.
++ widget = MultipleFileInput(attrs={"multiple": True})
++ value = widget.value_from_datadict(
++ data={"name": "Test name"},
++ files=MultiValueDict({"myfile": [file_1, file_2]}),
++ name="myfile",
++ )
++ self.assertEqual(value, [file_1, file_2])
++ # Uploading multiple files is not allowed.
++ widget = FileInput()
++ value = widget.value_from_datadict(
++ data={"name": "Test name"},
++ files=MultiValueDict({"myfile": [file_1, file_2]}),
++ name="myfile",
++ )
++ self.assertEqual(value, file_2)
++
++ def test_multiple_default(self):
++ class MultipleFileInput(FileInput):
++ allow_multiple_selected = True
++
++ tests = [
++ (None, True),
++ ({"class": "myclass"}, True),
++ ({"multiple": False}, False),
++ ]
++ for attrs, expected in tests:
++ widget = MultipleFileInput(attrs=attrs)
++ self.assertIs(widget.attrs["multiple"], expected)
diff -Nru python-django-2.2.28/debian/patches/CVE-2023-36053.patch python-django-2.2.28/debian/patches/CVE-2023-36053.patch
--- python-django-2.2.28/debian/patches/CVE-2023-36053.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-2.2.28/debian/patches/CVE-2023-36053.patch 2023-07-28 13:19:58.000000000 +0000
@@ -0,0 +1,202 @@
+From: Mariusz Felisiak
+Date: Wed, 14 Jun 2023 12:23:06 +0200
+Subject: [PATCH] [3.2.x] Fixed CVE-2023-36053 -- Prevented potential ReDoS in
+ EmailValidator and URLValidator.
+
+Thanks Seokchan Yoon for reports.
+---
+ django/core/validators.py | 7 ++++++-
+ django/forms/fields.py | 3 +++
+ docs/ref/validators.txt | 17 +++++++++++++++++
+ tests/forms_tests/field_tests/test_emailfield.py | 5 ++++-
+ tests/forms_tests/tests/test_forms.py | 19 +++++++++++++------
+ tests/validators/tests.py | 11 +++++++++++
+ 6 files changed, 54 insertions(+), 8 deletions(-)
+
+diff --git a/django/core/validators.py b/django/core/validators.py
+index a80d736f018f..4250139717b9 100644
+--- a/django/core/validators.py
++++ b/django/core/validators.py
+@@ -102,6 +102,7 @@ class URLValidator(RegexValidator):
+ message = _('Enter a valid URL.')
+ schemes = ['http', 'https', 'ftp', 'ftps']
+ unsafe_chars = frozenset('\t\r\n')
++ max_length = 2048
+
+ def __init__(self, schemes=None, **kwargs):
+ super().__init__(**kwargs)
+@@ -111,6 +112,8 @@ class URLValidator(RegexValidator):
+ def __call__(self, value):
+ if isinstance(value, str) and self.unsafe_chars.intersection(value):
+ raise ValidationError(self.message, code=self.code)
++ if not isinstance(value, str) or len(value) > self.max_length:
++ raise ValidationError(self.message, code=self.code, params={'value': value})
+ # Check if the scheme is valid.
+ scheme = value.split('://')[0].lower()
+ if scheme not in self.schemes:
+@@ -191,7 +194,9 @@ class EmailValidator:
+ self.domain_whitelist = whitelist
+
+ def __call__(self, value):
+- if not value or '@' not in value:
++ # The maximum length of an email is 320 characters per RFC 3696
++ # section 3.
++ if not value or '@' not in value or len(value) > 320:
+ raise ValidationError(self.message, code=self.code)
+
+ user_part, domain_part = value.rsplit('@', 1)
+diff --git a/django/forms/fields.py b/django/forms/fields.py
+index a97725652537..1185bfc77403 100644
+--- a/django/forms/fields.py
++++ b/django/forms/fields.py
+@@ -523,6 +523,9 @@ class EmailField(CharField):
+ default_validators = [validators.validate_email]
+
+ def __init__(self, **kwargs):
++ # The default maximum length of an email is 320 characters per RFC 3696
++ # section 3.
++ kwargs.setdefault("max_length", 320)
+ super().__init__(strip=True, **kwargs)
+
+
+diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt
+index 75d1394f0d77..d4f43acc2849 100644
+--- a/docs/ref/validators.txt
++++ b/docs/ref/validators.txt
+@@ -125,6 +125,11 @@ to, or in lieu of custom ``field.clean()`` methods.
+ :param code: If not ``None``, overrides :attr:`code`.
+ :param whitelist: If not ``None``, overrides :attr:`whitelist`.
+
++ An :class:`EmailValidator` ensures that a value looks like an email, and
++ raises a :exc:`~django.core.exceptions.ValidationError` with
++ :attr:`message` and :attr:`code` if it doesn't. Values longer than 320
++ characters are always considered invalid.
++
+ .. attribute:: message
+
+ The error message used by
+@@ -168,6 +173,18 @@ to, or in lieu of custom ``field.clean()`` methods.
+
+ .. _valid URI schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
+
++ .. attribute:: max_length
++
++ .. versionadded:: 3.2.20
++
++ The maximum length of values that could be considered valid. Defaults
++ to 2048 characters.
++
++ .. versionchanged:: 3.2.20
++
++ In older versions, values longer than 2048 characters could be
++ considered valid.
++
+ ``validate_email``
+ ------------------
+
+diff --git a/tests/forms_tests/field_tests/test_emailfield.py b/tests/forms_tests/field_tests/test_emailfield.py
+index 826524ae629d..fe5b644ab31e 100644
+--- a/tests/forms_tests/field_tests/test_emailfield.py
++++ b/tests/forms_tests/field_tests/test_emailfield.py
+@@ -8,7 +8,10 @@ class EmailFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
+
+ def test_emailfield_1(self):
+ f = EmailField()
+- self.assertWidgetRendersTo(f, '')
++ self.assertEqual(f.max_length, 320)
++ self.assertWidgetRendersTo(
++ f, ''
++ )
+ with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
+ f.clean('')
+ with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
+diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
+index d4e421d6ac9a..8893f893924c 100644
+--- a/tests/forms_tests/tests/test_forms.py
++++ b/tests/forms_tests/tests/test_forms.py
+@@ -422,11 +422,18 @@ class FormsTestCase(SimpleTestCase):
+ get_spam = BooleanField()
+
+ f = SignupForm(auto_id=False)
+- self.assertHTMLEqual(str(f['email']), '')
++ self.assertHTMLEqual(
++ str(f["email"]),
++ '',
++ )
+ self.assertHTMLEqual(str(f['get_spam']), '')
+
+ f = SignupForm({'email': 'test@example.com', 'get_spam': True}, auto_id=False)
+- self.assertHTMLEqual(str(f['email']), '')
++ self.assertHTMLEqual(
++ str(f["email"]),
++ '",
++ )
+ self.assertHTMLEqual(
+ str(f['get_spam']),
+ '',
+@@ -2780,7 +2787,7 @@ Good luck picking a username that doesn't already exist.
+
+
+
+-
++
+
This field is required.
+
"""
+ )
+@@ -2796,7 +2803,7 @@ Good luck picking a username that doesn't already exist.
+
+
+
+-
++
+
This field is required.
+
+
"""
+@@ -2815,7 +2822,7 @@ Good luck picking a username that doesn't already exist.
+
+
+
+-
++
+
+
This field is required.
+
"""
+@@ -3428,7 +3435,7 @@ Good luck picking a username that doesn't already exist.
+ f = CommentForm(data, auto_id=False, error_class=DivErrorList)
+ self.assertHTMLEqual(f.as_p(), """