Version in base suite: 3.2.19-1+deb12u1 Base version: python-django_3.2.19-1+deb12u1 Target version: python-django_3.2.19-1+deb12u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-django/python-django_3.2.19-1+deb12u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-django/python-django_3.2.19-1+deb12u2.dsc changelog | 23 +++ patches/0014-CVE-2023-36053.patch | 242 ++++++++++++++++++++++++++++++++++++ patches/0015-CVE-2024-39329.patch | 80 +++++++++++ patches/0016-CVE-2024-39330.patch | 145 +++++++++++++++++++++ patches/0017-CVE-2024-39614-1.patch | 127 ++++++++++++++++++ patches/0018-CVE-2024-39614-2.patch | 92 +++++++++++++ patches/0019-CVE-2024-41989.patch | 67 +++++++++ patches/0020-CVE-2024-41991.patch | 108 ++++++++++++++++ patches/0021-CVE-2024-42005.patch | 70 ++++++++++ patches/CVE-2023-36053.patch | 242 ------------------------------------ patches/series | 9 + 11 files changed, 962 insertions(+), 243 deletions(-) diff -Nru python-django-3.2.19/debian/changelog python-django-3.2.19/debian/changelog --- python-django-3.2.19/debian/changelog 2023-07-28 13:24:04.000000000 +0000 +++ python-django-3.2.19/debian/changelog 2024-08-21 11:08:24.000000000 +0000 @@ -1,3 +1,26 @@ +python-django (3:3.2.19-1+deb12u2) bookworm; urgency=high + + * Rename CVE-2023-36053.patch to 0014-CVE-2023-36053.patch + * Backport upstream fixes in 3:4.2.14-1: + * Closes: #1076069 + * CVE-2024-39329: Standardize timing of verify_password() when + checking unusable passwords. + * CVE-2024-39330: Add extra file name validation in Storage's save + method. + * CVE-2024-39614: Mitigate potential DoS in + get_supported_language_variant. + * The patch for CVE-2024-38875 won't sensibly backport. + * Backport upstream fixes in 3:4.2.15-1: + * Closes: #1078074 + * CVE-2024-41989: Prevent excessive memory consumption in floatformat. + * CVE-2024-41991: Prevente potential ReDoS in django.utils.html.urlize() + and AdminURLFieldWidget. + * CVE-2024-42005: Mitigate QuerySet.values() SQL injection attacks against JSON fields + Backport and tweak the upstream fix series to fit into 3.2. + * The patch for CVE-2024-41990 won't sensibly backport. + + -- Steve McIntyre <93sam@debian.org> Wed, 21 Aug 2024 12:08:24 +0100 + python-django (3:3.2.19-1+deb12u1) bookworm-security; urgency=high * CVE-2023-36053: Potential regular expression denial of service diff -Nru python-django-3.2.19/debian/patches/0014-CVE-2023-36053.patch python-django-3.2.19/debian/patches/0014-CVE-2023-36053.patch --- python-django-3.2.19/debian/patches/0014-CVE-2023-36053.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0014-CVE-2023-36053.patch 2024-08-07 15:56:53.000000000 +0000 @@ -0,0 +1,242 @@ +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/forms/fields.txt | 7 ++++++- + docs/ref/validators.txt | 25 +++++++++++++++++++++++- + tests/forms_tests/field_tests/test_emailfield.py | 5 ++++- + tests/forms_tests/tests/test_forms.py | 19 ++++++++++++------ + tests/validators/tests.py | 11 +++++++++++ + 7 files changed, 66 insertions(+), 11 deletions(-) + +diff --git a/django/core/validators.py b/django/core/validators.py +index 6b28eef08dd2..52ebddac6345 100644 +--- a/django/core/validators.py ++++ b/django/core/validators.py +@@ -93,6 +93,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) +@@ -100,7 +101,7 @@ class URLValidator(RegexValidator): + self.schemes = schemes + + def __call__(self, value): +- if not isinstance(value, str): ++ if not isinstance(value, str) or len(value) > self.max_length: + raise ValidationError(self.message, code=self.code, params={'value': value}) + if self.unsafe_chars.intersection(value): + raise ValidationError(self.message, code=self.code, params={'value': value}) +@@ -211,7 +212,9 @@ class EmailValidator: + self.domain_allowlist = allowlist + + 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, params={'value': value}) + + user_part, domain_part = value.rsplit('@', 1) +diff --git a/django/forms/fields.py b/django/forms/fields.py +index 0214d60c1cf1..8adb09e38294 100644 +--- a/django/forms/fields.py ++++ b/django/forms/fields.py +@@ -540,6 +540,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/forms/fields.txt b/docs/ref/forms/fields.txt +index 9438214a28ce..5b485f215384 100644 +--- a/docs/ref/forms/fields.txt ++++ b/docs/ref/forms/fields.txt +@@ -592,7 +592,12 @@ For each field, we describe the default widget used if you don't specify + * Error message keys: ``required``, ``invalid`` + + Has three optional arguments ``max_length``, ``min_length``, and +- ``empty_value`` which work just as they do for :class:`CharField`. ++ ``empty_value`` which work just as they do for :class:`CharField`. The ++ ``max_length`` argument defaults to 320 (see :rfc:`3696#section-3`). ++ ++ .. versionchanged:: 3.2.20 ++ ++ The default value for ``max_length`` was changed to 320 characters. + + ``FileField`` + ------------- +diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt +index 50761e5a425c..b22762b17b93 100644 +--- a/docs/ref/validators.txt ++++ b/docs/ref/validators.txt +@@ -130,6 +130,11 @@ to, or in lieu of custom ``field.clean()`` methods. + :param code: If not ``None``, overrides :attr:`code`. + :param allowlist: If not ``None``, overrides :attr:`allowlist`. + ++ 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 +@@ -158,13 +163,19 @@ to, or in lieu of custom ``field.clean()`` methods. + The undocumented ``domain_whitelist`` attribute is deprecated. Use + ``domain_allowlist`` instead. + ++ .. versionchanged:: 3.2.20 ++ ++ In older versions, values longer than 320 characters could be ++ considered valid. ++ + ``URLValidator`` + ---------------- + + .. class:: URLValidator(schemes=None, regex=None, message=None, code=None) + + A :class:`RegexValidator` subclass that ensures a value looks like a URL, +- and raises an error code of ``'invalid'`` if it doesn't. ++ and raises an error code of ``'invalid'`` if it doesn't. Values longer than ++ :attr:`max_length` characters are always considered invalid. + + Loopback addresses and reserved IP spaces are considered valid. Literal + IPv6 addresses (:rfc:`3986#section-3.2.2`) and Unicode domains are both +@@ -181,6 +192,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 8b85e4dcc144..19d315205d7e 100644 +--- a/tests/forms_tests/field_tests/test_emailfield.py ++++ b/tests/forms_tests/field_tests/test_emailfield.py +@@ -9,7 +9,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 26f8ecafea44..82a32af403a0 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']), + '', +@@ -2824,7 +2831,7 @@ Good luck picking a username that doesn't already exist.

+ + + +-
  • ++
  • +
  • +
  • """ + ) +@@ -2840,7 +2847,7 @@ Good luck picking a username that doesn't already exist.

    + + +

    +-

    ++

    + +

    +

    """ +@@ -2859,7 +2866,7 @@ Good luck picking a username that doesn't already exist.

    + + + +- ++ + + + """ +@@ -3489,7 +3496,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(), """

    Name:

    +
    Enter a valid email address.
    +-

    Email:

    ++

    Email:

    +
    This field is required.
    +

    Comment:

    """) + +diff --git a/tests/validators/tests.py b/tests/validators/tests.py +index e39d0e3a1cef..1065727a974e 100644 +--- a/tests/validators/tests.py ++++ b/tests/validators/tests.py +@@ -59,6 +59,7 @@ TEST_DATA = [ + + (validate_email, 'example@atm.%s' % ('a' * 64), ValidationError), + (validate_email, 'example@%s.atm.%s' % ('b' * 64, 'a' * 63), ValidationError), ++ (validate_email, "example@%scom" % (("a" * 63 + ".") * 100), ValidationError), + (validate_email, None, ValidationError), + (validate_email, '', ValidationError), + (validate_email, 'abc', ValidationError), +@@ -246,6 +247,16 @@ TEST_DATA = [ + (URLValidator(), None, ValidationError), + (URLValidator(), 56, ValidationError), + (URLValidator(), 'no_scheme', ValidationError), ++ ( ++ URLValidator(), ++ "http://example." + ("a" * 63 + ".") * 1000 + "com", ++ ValidationError, ++ ), ++ ( ++ URLValidator(), ++ "http://userid:password" + "d" * 2000 + "@example.aaaaaaaaaaaaa.com", ++ None, ++ ), + # Newlines and tabs are not accepted. + (URLValidator(), 'http://www.djangoproject.com/\n', ValidationError), + (URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError), diff -Nru python-django-3.2.19/debian/patches/0015-CVE-2024-39329.patch python-django-3.2.19/debian/patches/0015-CVE-2024-39329.patch --- python-django-3.2.19/debian/patches/0015-CVE-2024-39329.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0015-CVE-2024-39329.patch 2024-08-21 11:08:24.000000000 +0000 @@ -0,0 +1,80 @@ +commit 5d8645857936c142a3973694799c52165e2bdcdb +Author: Michael Manfre +Date: Fri Jun 14 22:12:58 2024 -0400 + + Fixed CVE-2024-39329 -- Standarized timing of verify_password() when checking unusuable passwords. + + Refs #20760. + + Thanks Michael Manfre for the fix and to Adam Johnson for the review. + +diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py +index 86ae7f42a..ee81b641d 100644 +--- a/django/contrib/auth/hashers.py ++++ b/django/contrib/auth/hashers.py +@@ -36,14 +36,20 @@ def check_password(password, encoded, setter=None, preferred='default'): + If setter is specified, it'll be called when you need to + regenerate the password. + """ +- if password is None or not is_password_usable(encoded): +- return False ++ fake_runtime = password is None or not is_password_usable(encoded) + + preferred = get_hasher(preferred) + try: + hasher = identify_hasher(encoded) + except ValueError: + # encoded is gibberish or uses a hasher that's no longer installed. ++ fake_runtime = True ++ ++ if fake_runtime: ++ # Run the default password hasher once to reduce the timing difference ++ # between an existing user with an unusable password and a nonexistent ++ # user or missing hasher (similar to #20760). ++ make_password(get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)) + return False + + hasher_changed = hasher.algorithm != preferred.algorithm +diff --git a/tests/auth_tests/test_hashers.py b/tests/auth_tests/test_hashers.py +index 8bc61bc8b..a1ae9400e 100644 +--- a/tests/auth_tests/test_hashers.py ++++ b/tests/auth_tests/test_hashers.py +@@ -474,6 +474,38 @@ class TestUtilsHashPass(SimpleTestCase): + check_password('wrong_password', encoded) + self.assertEqual(hasher.harden_runtime.call_count, 1) + ++ def test_check_password_calls_make_password_to_fake_runtime(self): ++ hasher = get_hasher("default") ++ cases = [ ++ (None, None, None), # no plain text password provided ++ ("foo", make_password(password=None), None), # unusable encoded ++ ("letmein", make_password(password="letmein"), ValueError), # valid encoded ++ ] ++ for password, encoded, hasher_side_effect in cases: ++ with ( ++ self.subTest(encoded=encoded), ++ mock.patch( ++ "django.contrib.auth.hashers.identify_hasher", ++ side_effect=hasher_side_effect, ++ ) as mock_identify_hasher, ++ mock.patch( ++ "django.contrib.auth.hashers.make_password" ++ ) as mock_make_password, ++ mock.patch( ++ "django.contrib.auth.hashers.get_random_string", ++ side_effect=lambda size: "x" * size, ++ ), ++ mock.patch.object(hasher, "verify"), ++ ): ++ # Ensure make_password is called to standardize timing. ++ check_password(password, encoded) ++ self.assertEqual(hasher.verify.call_count, 0) ++ self.assertEqual(mock_identify_hasher.mock_calls, [mock.call(encoded)]) ++ self.assertEqual( ++ mock_make_password.mock_calls, ++ [mock.call("x" * UNUSABLE_PASSWORD_SUFFIX_LENGTH)], ++ ) ++ + + class BasePasswordHasherTests(SimpleTestCase): + not_implemented_msg = 'subclasses of BasePasswordHasher must provide %s() method' diff -Nru python-django-3.2.19/debian/patches/0016-CVE-2024-39330.patch python-django-3.2.19/debian/patches/0016-CVE-2024-39330.patch --- python-django-3.2.19/debian/patches/0016-CVE-2024-39330.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0016-CVE-2024-39330.patch 2024-08-21 11:08:24.000000000 +0000 @@ -0,0 +1,145 @@ +commit fe4a0bbe2088d0c2b331216dad21ccd0bb3ee80d +Author: Natalia <124304+nessita@users.noreply.github.com> +Date: Wed Mar 20 13:55:21 2024 -0300 + + Fixed CVE-2024-39330 -- Added extra file name validation in Storage's save method. + + Thanks to Josh Schneier for the report, and to Carlton Gibson and Sarah + Boyce for the reviews. + +diff --git a/django/core/files/storage.py b/django/core/files/storage.py +index 22984f949..680f5ec91 100644 +--- a/django/core/files/storage.py ++++ b/django/core/files/storage.py +@@ -50,7 +50,18 @@ class Storage: + if not hasattr(content, 'chunks'): + content = File(content, name) + ++ # Ensure that the name is valid, before and after having the storage ++ # system potentially modifying the name. This duplicates the check made ++ # inside `get_available_name` but it's necessary for those cases where ++ # `get_available_name` is overriden and validation is lost. ++ validate_file_name(name, allow_relative_path=True) ++ ++ # Potentially find a different name depending on storage constraints. + name = self.get_available_name(name, max_length=max_length) ++ # Validate the (potentially) new name. ++ validate_file_name(name, allow_relative_path=True) ++ ++ # The save operation should return the actual name of the file saved. + name = self._save(name, content) + # Ensure that the name returned from the storage system is still valid. + validate_file_name(name, allow_relative_path=True) +diff --git a/django/core/files/utils.py b/django/core/files/utils.py +index 611f932f6e..c730ca17e8 100644 +--- a/django/core/files/utils.py ++++ b/django/core/files/utils.py +@@ -10,10 +10,9 @@ def validate_file_name(name, allow_relative_path=False): + raise SuspiciousFileOperation("Could not derive file name from '%s'" % name) + + if allow_relative_path: +- # Use PurePosixPath() because this branch is checked only in +- # FileField.generate_filename() where all file paths are expected to be +- # Unix style (with forward slashes). +- path = pathlib.PurePosixPath(name) ++ # Ensure that name can be treated as a pure posix path, i.e. Unix ++ # style (with forward slashes). ++ path = pathlib.PurePosixPath(str(name).replace("\\", "/")) + if path.is_absolute() or '..' in path.parts: + raise SuspiciousFileOperation( + "Detected path traversal attempt in '%s'" % name +diff --git a/tests/file_storage/test_base.py b/tests/file_storage/test_base.py +new file mode 100644 +index 0000000000..712d3ba2e2 +--- /dev/null ++++ b/tests/file_storage/test_base.py +@@ -0,0 +1,72 @@ ++import os ++from unittest import mock ++ ++from django.core.exceptions import SuspiciousFileOperation ++from django.core.files.storage import Storage ++from django.test import SimpleTestCase ++ ++ ++class CustomStorage(Storage): ++ """Simple Storage subclass implementing the bare minimum for testing.""" ++ ++ def exists(self, name): ++ return False ++ ++ def _save(self, name): ++ return name ++ ++ ++class StorageValidateFileNameTests(SimpleTestCase): ++ ++ invalid_file_names = [ ++ os.path.join("path", "to", os.pardir, "test.file"), ++ os.path.join(os.path.sep, "path", "to", "test.file"), ++ ] ++ error_msg = "Detected path traversal attempt in '%s'" ++ ++ def test_validate_before_get_available_name(self): ++ s = CustomStorage() ++ # The initial name passed to `save` is not valid nor safe, fail early. ++ for name in self.invalid_file_names: ++ with ( ++ self.subTest(name=name), ++ mock.patch.object(s, "get_available_name") as mock_get_available_name, ++ mock.patch.object(s, "_save") as mock_internal_save, ++ ): ++ with self.assertRaisesMessage( ++ SuspiciousFileOperation, self.error_msg % name ++ ): ++ s.save(name, content="irrelevant") ++ self.assertEqual(mock_get_available_name.mock_calls, []) ++ self.assertEqual(mock_internal_save.mock_calls, []) ++ ++ def test_validate_after_get_available_name(self): ++ s = CustomStorage() ++ # The initial name passed to `save` is valid and safe, but the returned ++ # name from `get_available_name` is not. ++ for name in self.invalid_file_names: ++ with ( ++ self.subTest(name=name), ++ mock.patch.object(s, "get_available_name", return_value=name), ++ mock.patch.object(s, "_save") as mock_internal_save, ++ ): ++ with self.assertRaisesMessage( ++ SuspiciousFileOperation, self.error_msg % name ++ ): ++ s.save("valid-file-name.txt", content="irrelevant") ++ self.assertEqual(mock_internal_save.mock_calls, []) ++ ++ def test_validate_after_internal_save(self): ++ s = CustomStorage() ++ # The initial name passed to `save` is valid and safe, but the result ++ # from `_save` is not (this is achieved by monkeypatching _save). ++ for name in self.invalid_file_names: ++ with ( ++ self.subTest(name=name), ++ mock.patch.object(s, "_save", return_value=name), ++ ): ++ ++ with self.assertRaisesMessage( ++ SuspiciousFileOperation, self.error_msg % name ++ ): ++ s.save("valid-file-name.txt", content="irrelevant") +diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py +index 723809324..6d17a7118 100644 +--- a/tests/file_storage/tests.py ++++ b/tests/file_storage/tests.py +@@ -297,12 +297,6 @@ class FileStorageTests(SimpleTestCase): + + self.storage.delete('path/to/test.file') + +- def test_file_save_abs_path(self): +- test_name = 'path/to/test.file' +- f = ContentFile('file saved with path') +- f_name = self.storage.save(os.path.join(self.temp_dir, test_name), f) +- self.assertEqual(f_name, test_name) +- + def test_save_doesnt_close(self): + with TemporaryUploadedFile('test', 'text/plain', 1, 'utf8') as file: + file.write(b'1') diff -Nru python-django-3.2.19/debian/patches/0017-CVE-2024-39614-1.patch python-django-3.2.19/debian/patches/0017-CVE-2024-39614-1.patch --- python-django-3.2.19/debian/patches/0017-CVE-2024-39614-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0017-CVE-2024-39614-1.patch 2024-08-21 11:08:24.000000000 +0000 @@ -0,0 +1,127 @@ +commit 9e9792228a6bb5d6402a5d645bc3be4cf364aefb +Author: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> +Date: Wed Jun 26 12:11:54 2024 +0200 + + Fixed CVE-2024-39614 -- Mitigated potential DoS in get_supported_language_variant(). + + Language codes are now parsed with a maximum length limit of 500 chars. + + Thanks to MProgrammer for the report. + +diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py +index b262a5000..92442185f 100644 +--- a/django/utils/translation/trans_real.py ++++ b/django/utils/translation/trans_real.py +@@ -31,9 +31,10 @@ _default = None + 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 ++# header or cookie to prevent possible denial of service or memory exhaustion ++# attacks. About 10x longer than the longest value shown on MDN’s ++# Accept-Language page. ++LANGUAGE_CODE_MAX_LENGTH = 500 + + # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9 + # and RFC 3066, section 2.1 +@@ -474,11 +475,25 @@ def get_supported_language_variant(lang_code, strict=False): + If `strict` is False (the default), look for a country-specific variant + when neither the language code nor its generic variant is found. + ++ The language code is truncated to a maximum length to avoid potential ++ denial of service attacks. ++ + lru_cache should have a maxsize to prevent from memory exhaustion attacks, + as the provided language codes are taken from the HTTP request. See also + . + """ + if lang_code: ++ # Truncate the language code to a maximum length to avoid potential ++ # denial of service attacks. ++ if len(lang_code) > LANGUAGE_CODE_MAX_LENGTH: ++ if ( ++ not strict ++ and (index := lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)) > 0 ++ ): ++ # There is a generic variant under the maximum length accepted length. ++ lang_code = lang_code[:index] ++ else: ++ raise ValueError("'lang_code' exceeds the maximum accepted length") + # If 'fr-ca' is not supported, try special fallback or language-only 'fr'. + possible_lang_codes = [lang_code] + try: +@@ -595,13 +610,13 @@ def parse_accept_lang_header(lang_string): + 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: ++ if len(lang_string) <= LANGUAGE_CODE_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) ++ index = lang_string.rfind(",", 0, LANGUAGE_CODE_MAX_LENGTH) + if index > 0: + return _parse_accept_lang_header(lang_string[:index]) + +diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt +index ce3a4cba0..00a3f5e79 100644 +--- a/docs/ref/utils.txt ++++ b/docs/ref/utils.txt +@@ -1129,6 +1129,11 @@ For a complete discussion on the usage of the following see the + ``lang_code`` is ``'es-ar'`` and ``'es'`` is in :setting:`LANGUAGES` but + ``'es-ar'`` isn't. + ++ ``lang_code`` has a maximum accepted length of 500 characters. A ++ :exc:`ValueError` is raised if ``lang_code`` exceeds this limit and ++ ``strict`` is ``True``, or if there is no generic variant and ``strict`` ++ is ``False``. ++ + If ``strict`` is ``False`` (the default), a country-specific variant may + be returned when neither the language code nor its generic variant is found. + For example, if only ``'es-co'`` is in :setting:`LANGUAGES`, that's +@@ -1137,6 +1142,11 @@ For a complete discussion on the usage of the following see the + + Raises :exc:`LookupError` if nothing is found. + ++ .. versionchanged:: 4.2.14 ++ ++ In older versions, ``lang_code`` values over 500 characters were ++ processed without raising a :exc:`ValueError`. ++ + .. function:: to_locale(language) + + Turns a language name (en-us) into a locale name (en_US). +diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py +index 41ec63da9..793bd3db8 100644 +--- a/tests/i18n/tests.py ++++ b/tests/i18n/tests.py +@@ -40,6 +40,7 @@ from django.utils.translation import ( + from django.utils.translation.reloader import ( + translation_file_changed, watch_for_translation_changes, + ) ++from django.utils.translation.trans_real import LANGUAGE_CODE_MAX_LENGTH + + from .forms import CompanyForm, I18nForm, SelectDateForm + from .models import Company, TestModel +@@ -1532,6 +1533,16 @@ class MiscTests(SimpleTestCase): + g('xyz') + with self.assertRaises(LookupError): + g('xy-zz') ++ msg = "'lang_code' exceeds the maximum accepted length" ++ with self.assertRaises(LookupError): ++ g("x" * LANGUAGE_CODE_MAX_LENGTH) ++ with self.assertRaisesMessage(ValueError, msg): ++ g("x" * (LANGUAGE_CODE_MAX_LENGTH + 1)) ++ # 167 * 3 = 501 which is LANGUAGE_CODE_MAX_LENGTH + 1. ++ self.assertEqual(g("en-" * 167), "en") ++ with self.assertRaisesMessage(ValueError, msg): ++ g("en-" * 167, strict=True) ++ self.assertEqual(g("en-" * 30000), "en") # catastrophic test + + def test_get_supported_language_variant_null(self): + g = trans_null.get_supported_language_variant diff -Nru python-django-3.2.19/debian/patches/0018-CVE-2024-39614-2.patch python-django-3.2.19/debian/patches/0018-CVE-2024-39614-2.patch --- python-django-3.2.19/debian/patches/0018-CVE-2024-39614-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0018-CVE-2024-39614-2.patch 2024-08-21 11:08:24.000000000 +0000 @@ -0,0 +1,92 @@ +commit 0e94f292cda632153f2b3d9a9037eb0141ae9c2e +Author: Lorenzo Peña +Date: Tue Jul 23 12:06:29 2024 +0200 + + Fixed #35627 -- Raised a LookupError rather than an unhandled ValueError in get_supported_language_variant(). + + LocaleMiddleware didn't handle the ValueError raised by + get_supported_language_variant() when language codes were + over 500 characters. + + Regression in 9e9792228a6bb5d6402a5d645bc3be4cf364aefb. + +Index: python-django-debian.git/django/utils/translation/trans_real.py +=================================================================== +--- python-django-debian.git.orig/django/utils/translation/trans_real.py ++++ python-django-debian.git/django/utils/translation/trans_real.py +@@ -493,7 +493,7 @@ def get_supported_language_variant(lang_ + # There is a generic variant under the maximum length accepted length. + lang_code = lang_code[:index] + else: +- raise ValueError("'lang_code' exceeds the maximum accepted length") ++ raise LookupError(lang_code) + # If 'fr-ca' is not supported, try special fallback or language-only 'fr'. + possible_lang_codes = [lang_code] + try: +Index: python-django-debian.git/docs/ref/utils.txt +=================================================================== +--- python-django-debian.git.orig/docs/ref/utils.txt ++++ python-django-debian.git/docs/ref/utils.txt +@@ -1130,7 +1130,7 @@ For a complete discussion on the usage o + ``'es-ar'`` isn't. + + ``lang_code`` has a maximum accepted length of 500 characters. A +- :exc:`ValueError` is raised if ``lang_code`` exceeds this limit and ++ :exc:`LookupError` is raised if ``lang_code`` exceeds this limit and + ``strict`` is ``True``, or if there is no generic variant and ``strict`` + is ``False``. + +@@ -1142,10 +1142,10 @@ For a complete discussion on the usage o + + Raises :exc:`LookupError` if nothing is found. + +- .. versionchanged:: 4.2.14 ++ .. versionchanged:: 4.2.15 + + In older versions, ``lang_code`` values over 500 characters were +- processed without raising a :exc:`ValueError`. ++ processed without raising a :exc:`LookupError`. + + .. function:: to_locale(language) + +Index: python-django-debian.git/tests/i18n/tests.py +=================================================================== +--- python-django-debian.git.orig/tests/i18n/tests.py ++++ python-django-debian.git/tests/i18n/tests.py +@@ -1533,14 +1533,13 @@ class MiscTests(SimpleTestCase): + g('xyz') + with self.assertRaises(LookupError): + g('xy-zz') +- msg = "'lang_code' exceeds the maximum accepted length" + with self.assertRaises(LookupError): + g("x" * LANGUAGE_CODE_MAX_LENGTH) +- with self.assertRaisesMessage(ValueError, msg): ++ with self.assertRaises(LookupError): + g("x" * (LANGUAGE_CODE_MAX_LENGTH + 1)) + # 167 * 3 = 501 which is LANGUAGE_CODE_MAX_LENGTH + 1. + self.assertEqual(g("en-" * 167), "en") +- with self.assertRaisesMessage(ValueError, msg): ++ with self.assertRaises(LookupError): + g("en-" * 167, strict=True) + self.assertEqual(g("en-" * 30000), "en") # catastrophic test + +@@ -1579,6 +1578,7 @@ class MiscTests(SimpleTestCase): + self.assertEqual(g('/de-at/'), 'de-at') + self.assertEqual(g('/de-ch/'), 'de') + self.assertIsNone(g('/de-simple-page/')) ++ self.assertIsNone(g(f"/{'a' * 501}/")) + + def test_get_language_from_path_null(self): + g = trans_null.get_language_from_path +@@ -1866,6 +1866,11 @@ class CountrySpecificLanguageTests(Simpl + lang = get_language_from_request(r) + self.assertEqual('bg', lang) + ++ def test_get_language_from_request_code_too_long(self): ++ request = self.rf.get("/", headers={"accept-language": "a" * 501}) ++ lang = get_language_from_request(request) ++ self.assertEqual("en-us", lang) ++ + def test_get_language_from_request_null(self): + lang = trans_null.get_language_from_request(None) + self.assertEqual(lang, 'en') diff -Nru python-django-3.2.19/debian/patches/0019-CVE-2024-41989.patch python-django-3.2.19/debian/patches/0019-CVE-2024-41989.patch --- python-django-3.2.19/debian/patches/0019-CVE-2024-41989.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0019-CVE-2024-41989.patch 2024-08-21 11:08:24.000000000 +0000 @@ -0,0 +1,67 @@ +commit c19465ad87e33b6122c886b97a202ad54cd43672 +Author: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> +Date: Fri Jul 12 11:38:34 2024 +0200 + + Fixed CVE-2024-41989 -- Prevented excessive memory consumption in floatformat. + + Thanks Elias Myllymäki for the report. + + Co-authored-by: Shai Berger + +diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py +index 02cac06bcf..66c6e76d20 100644 +--- a/django/template/defaultfilters.py ++++ b/django/template/defaultfilters.py +@@ -146,6 +146,19 @@ def floatformat(text, arg=-1): + except ValueError: + return input_val + ++ _, digits, exponent = d.as_tuple() ++ try: ++ number_of_digits_and_exponent_sum = len(digits) + abs(exponent) ++ except TypeError: ++ # Exponent values can be "F", "n", "N". ++ number_of_digits_and_exponent_sum = 0 ++ ++ # Values with more than 200 digits, or with a large exponent, are returned "as is" ++ # to avoid high memory consumption and potential denial-of-service attacks. ++ # The cut-off of 200 is consistent with django.utils.numberformat.floatformat(). ++ if number_of_digits_and_exponent_sum > 200: ++ return input_val ++ + try: + m = int(d) - d + except (ValueError, OverflowError, InvalidOperation): +diff --git a/tests/template_tests/filter_tests/test_floatformat.py b/tests/template_tests/filter_tests/test_floatformat.py +index 145858b75f..3d6c34a552 100644 +--- a/tests/template_tests/filter_tests/test_floatformat.py ++++ b/tests/template_tests/filter_tests/test_floatformat.py +@@ -59,6 +59,7 @@ class FunctionTests(SimpleTestCase): + self.assertEqual(floatformat(1.5e-15, 20), '0.00000000000000150000') + self.assertEqual(floatformat(1.5e-15, -20), '0.00000000000000150000') + self.assertEqual(floatformat(1.00000000000000015, 16), '1.0000000000000002') ++ self.assertEqual(floatformat("1e199"), "1" + "0" * 199) + + @override_settings(USE_L10N=True) + def test_force_grouping(self): +@@ -96,6 +97,20 @@ class FunctionTests(SimpleTestCase): + self.assertEqual(floatformat(pos_inf), 'inf') + self.assertEqual(floatformat(neg_inf), '-inf') + self.assertEqual(floatformat(pos_inf / pos_inf), 'nan') ++ ++ def test_too_many_digits_to_render(self): ++ cases = [ ++ "1e200", ++ "1E200", ++ "1E10000000000000000", ++ "-1E10000000000000000", ++ "1e10000000000000000", ++ "-1e10000000000000000", ++ "1" + "0" * 1_000_000, ++ ] ++ for value in cases: ++ with self.subTest(value=value): ++ self.assertEqual(floatformat(value), "'" + value + "'") + + def test_float_dunder_method(self): + class FloatWrapper: diff -Nru python-django-3.2.19/debian/patches/0020-CVE-2024-41991.patch python-django-3.2.19/debian/patches/0020-CVE-2024-41991.patch --- python-django-3.2.19/debian/patches/0020-CVE-2024-41991.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0020-CVE-2024-41991.patch 2024-08-21 11:08:24.000000000 +0000 @@ -0,0 +1,108 @@ +commit 5f1757142febd95994caa1c0f64c1a0c161982c3 +Author: Mariusz Felisiak +Date: Wed Jul 10 20:30:12 2024 +0200 + + Fixed CVE-2024-41991 -- Prevented potential ReDoS in django.utils.html.urlize() and AdminURLFieldWidget. + + Thanks Seokchan Yoon for the report. + + Co-authored-by: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com> + +diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py +index aeb74773a..b7dd0d87a 100644 +--- a/django/contrib/admin/widgets.py ++++ b/django/contrib/admin/widgets.py +@@ -339,7 +339,7 @@ class AdminURLFieldWidget(forms.URLInput): + context = super().get_context(name, value, attrs) + context['current_label'] = _('Currently:') + context['change_label'] = _('Change:') +- context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else '' ++ context['widget']['href'] = smart_urlquote(context['widget']['value']) if url_valid else '' + context['url_valid'] = url_valid + return context + +diff --git a/django/utils/html.py b/django/utils/html.py +index 1123e38f6c..154c820d34 100644 +--- a/django/utils/html.py ++++ b/django/utils/html.py +@@ -28,7 +28,7 @@ simple_url_2_re = _lazy_re_compile( + r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)($|/.*)$', + re.IGNORECASE + ) +- ++MAX_URL_LENGTH = 2048 + + @keep_lazy(str, SafeString) + def escape(text): +@@ -298,6 +298,10 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): + except ValueError: + # value contains more than one @. + return False ++ # Max length for domain name labels is 63 characters per RFC 1034. ++ # Helps to avoid ReDoS vectors in the domain part. ++ if len(p2) > 63: ++ return False + # Dot must be in p2 (e.g. example.com) + if '.' not in p2 or p2.startswith('.'): + return False +@@ -316,9 +322,9 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): + # Make URL we want to point to. + url = None + nofollow_attr = ' rel="nofollow"' if nofollow else '' +- if simple_url_re.match(middle): ++ if len(middle) <= MAX_URL_LENGTH and simple_url_re.match(middle): + url = smart_urlquote(html.unescape(middle)) +- elif simple_url_2_re.match(middle): ++ elif len(middle) <= MAX_URL_LENGTH and simple_url_2_re.match(middle): + url = smart_urlquote('http://%s' % html.unescape(middle)) + elif ':' not in middle and is_email_simple(middle): + local, domain = middle.rsplit('@', 1) +diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py +index 517e060b80..5da4adf8c9 100644 +--- a/tests/admin_widgets/tests.py ++++ b/tests/admin_widgets/tests.py +@@ -353,7 +353,12 @@ class AdminSplitDateTimeWidgetTest(SimpleTestCase): + class AdminURLWidgetTest(SimpleTestCase): + def test_get_context_validates_url(self): + w = widgets.AdminURLFieldWidget() +- for invalid in ['', '/not/a/full/url/', 'javascript:alert("Danger XSS!")']: ++ for invalid in [ ++ "", ++ "/not/a/full/url/", ++ 'javascript:alert("Danger XSS!")', ++ "http://" + "한.글." * 1_000_000 + "com", ++ ]: + with self.subTest(url=invalid): + self.assertFalse(w.get_context('name', invalid, {})['url_valid']) + self.assertTrue(w.get_context('name', 'http://example.com', {})['url_valid']) +diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py +index 30f5ba68e..93458acd1 100644 +--- a/tests/utils_tests/test_html.py ++++ b/tests/utils_tests/test_html.py +@@ -255,6 +255,15 @@ class TestUtilsHtml(SimpleTestCase): + 'Search for google.com/?q=!' + ), + ('foo@example.com', 'foo@example.com'), ++ ( ++ "test@" + "한.글." * 15 + "aaa", ++ '' ++ + "test@" ++ + "한.글." * 15 ++ + "aaa", ++ ), + ) + for value, output in tests: + with self.subTest(value=value): +@@ -263,6 +272,10 @@ class TestUtilsHtml(SimpleTestCase): + def test_urlize_unchanged_inputs(self): + tests = ( + ('a' + '@a' * 50000) + 'a', # simple_email_re catastrophic test ++ # Unicode domain catastrophic tests. ++ "a@" + "한.글." * 1_000_000 + "a", ++ "http://" + "한.글." * 1_000_000 + "com", ++ "www." + "한.글." * 1_000_000 + "com", + ('a' + '.' * 1000000) + 'a', # trailing_punctuation catastrophic test + 'foo@', + '@foo.com', diff -Nru python-django-3.2.19/debian/patches/0021-CVE-2024-42005.patch python-django-3.2.19/debian/patches/0021-CVE-2024-42005.patch --- python-django-3.2.19/debian/patches/0021-CVE-2024-42005.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-3.2.19/debian/patches/0021-CVE-2024-42005.patch 2024-08-21 11:08:24.000000000 +0000 @@ -0,0 +1,70 @@ +commit c87bfaacf8fb84984243b5055dc70f97996cb115 +Author: Simon Charette +Date: Thu Jul 25 12:19:13 2024 -0400 + + Fixed CVE-2024-42005 -- Mitigated QuerySet.values() SQL injection attacks against JSON fields. + + Thanks Eyal (eyalgabay) for the report. + +Index: python-django-debian.git/django/db/models/sql/query.py +=================================================================== +--- python-django-debian.git.orig/django/db/models/sql/query.py ++++ python-django-debian.git/django/db/models/sql/query.py +@@ -2239,6 +2239,8 @@ class Query(BaseExpression): + self.clear_select_fields() + + if fields: ++ for field in fields: ++ self.check_alias(field) + field_names = [] + extra_names = [] + annotation_names = [] +Index: python-django-debian.git/tests/expressions/models.py +=================================================================== +--- python-django-debian.git.orig/tests/expressions/models.py ++++ python-django-debian.git/tests/expressions/models.py +@@ -101,3 +101,10 @@ class UUIDPK(models.Model): + class UUID(models.Model): + uuid = models.UUIDField(null=True) + uuid_fk = models.ForeignKey(UUIDPK, models.CASCADE, null=True) ++ ++ ++class JSONFieldModel(models.Model): ++ data = models.JSONField(null=True) ++ ++ class Meta: ++ required_db_features = {"supports_json_field"} +Index: python-django-debian.git/tests/expressions/test_queryset_values.py +=================================================================== +--- python-django-debian.git.orig/tests/expressions/test_queryset_values.py ++++ python-django-debian.git/tests/expressions/test_queryset_values.py +@@ -1,7 +1,7 @@ + from django.db.models import F, Sum +-from django.test import TestCase ++from django.test import TestCase, skipUnlessDBFeature + +-from .models import Company, Employee ++from .models import Company, Employee, JSONFieldModel + + + class ValuesExpressionsTests(TestCase): +@@ -35,6 +35,19 @@ class ValuesExpressionsTests(TestCase): + with self.assertRaisesMessage(ValueError, msg): + Company.objects.values(**{crafted_alias: F("ceo__salary")}) + ++ @skipUnlessDBFeature("supports_json_field") ++ def test_values_expression_alias_sql_injection_json_field(self): ++ crafted_alias = """injected_name" from "expressions_company"; --""" ++ msg = ( ++ "Column aliases cannot contain whitespace characters, quotation marks, " ++ "semicolons, or SQL comments." ++ ) ++ with self.assertRaisesMessage(ValueError, msg): ++ JSONFieldModel.objects.values(f"data__{crafted_alias}") ++ ++ with self.assertRaisesMessage(ValueError, msg): ++ JSONFieldModel.objects.values_list(f"data__{crafted_alias}") ++ + def test_values_expression_group_by(self): + # values() applies annotate() first, so values selected are grouped by + # id, not firstname. diff -Nru python-django-3.2.19/debian/patches/CVE-2023-36053.patch python-django-3.2.19/debian/patches/CVE-2023-36053.patch --- python-django-3.2.19/debian/patches/CVE-2023-36053.patch 2023-07-28 13:24:04.000000000 +0000 +++ python-django-3.2.19/debian/patches/CVE-2023-36053.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,242 +0,0 @@ -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/forms/fields.txt | 7 ++++++- - docs/ref/validators.txt | 25 +++++++++++++++++++++++- - tests/forms_tests/field_tests/test_emailfield.py | 5 ++++- - tests/forms_tests/tests/test_forms.py | 19 ++++++++++++------ - tests/validators/tests.py | 11 +++++++++++ - 7 files changed, 66 insertions(+), 11 deletions(-) - -diff --git a/django/core/validators.py b/django/core/validators.py -index 6b28eef08dd2..52ebddac6345 100644 ---- a/django/core/validators.py -+++ b/django/core/validators.py -@@ -93,6 +93,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) -@@ -100,7 +101,7 @@ class URLValidator(RegexValidator): - self.schemes = schemes - - def __call__(self, value): -- if not isinstance(value, str): -+ if not isinstance(value, str) or len(value) > self.max_length: - raise ValidationError(self.message, code=self.code, params={'value': value}) - if self.unsafe_chars.intersection(value): - raise ValidationError(self.message, code=self.code, params={'value': value}) -@@ -211,7 +212,9 @@ class EmailValidator: - self.domain_allowlist = allowlist - - 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, params={'value': value}) - - user_part, domain_part = value.rsplit('@', 1) -diff --git a/django/forms/fields.py b/django/forms/fields.py -index 0214d60c1cf1..8adb09e38294 100644 ---- a/django/forms/fields.py -+++ b/django/forms/fields.py -@@ -540,6 +540,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/forms/fields.txt b/docs/ref/forms/fields.txt -index 9438214a28ce..5b485f215384 100644 ---- a/docs/ref/forms/fields.txt -+++ b/docs/ref/forms/fields.txt -@@ -592,7 +592,12 @@ For each field, we describe the default widget used if you don't specify - * Error message keys: ``required``, ``invalid`` - - Has three optional arguments ``max_length``, ``min_length``, and -- ``empty_value`` which work just as they do for :class:`CharField`. -+ ``empty_value`` which work just as they do for :class:`CharField`. The -+ ``max_length`` argument defaults to 320 (see :rfc:`3696#section-3`). -+ -+ .. versionchanged:: 3.2.20 -+ -+ The default value for ``max_length`` was changed to 320 characters. - - ``FileField`` - ------------- -diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt -index 50761e5a425c..b22762b17b93 100644 ---- a/docs/ref/validators.txt -+++ b/docs/ref/validators.txt -@@ -130,6 +130,11 @@ to, or in lieu of custom ``field.clean()`` methods. - :param code: If not ``None``, overrides :attr:`code`. - :param allowlist: If not ``None``, overrides :attr:`allowlist`. - -+ 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 -@@ -158,13 +163,19 @@ to, or in lieu of custom ``field.clean()`` methods. - The undocumented ``domain_whitelist`` attribute is deprecated. Use - ``domain_allowlist`` instead. - -+ .. versionchanged:: 3.2.20 -+ -+ In older versions, values longer than 320 characters could be -+ considered valid. -+ - ``URLValidator`` - ---------------- - - .. class:: URLValidator(schemes=None, regex=None, message=None, code=None) - - A :class:`RegexValidator` subclass that ensures a value looks like a URL, -- and raises an error code of ``'invalid'`` if it doesn't. -+ and raises an error code of ``'invalid'`` if it doesn't. Values longer than -+ :attr:`max_length` characters are always considered invalid. - - Loopback addresses and reserved IP spaces are considered valid. Literal - IPv6 addresses (:rfc:`3986#section-3.2.2`) and Unicode domains are both -@@ -181,6 +192,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 8b85e4dcc144..19d315205d7e 100644 ---- a/tests/forms_tests/field_tests/test_emailfield.py -+++ b/tests/forms_tests/field_tests/test_emailfield.py -@@ -9,7 +9,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 26f8ecafea44..82a32af403a0 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']), - '', -@@ -2824,7 +2831,7 @@ Good luck picking a username that doesn't already exist.

    - - - --
  • -+
  • -
    • This field is required.
    -
  • """ - ) -@@ -2840,7 +2847,7 @@ Good luck picking a username that doesn't already exist.

    - - -

    --

    -+

    -
    • This field is required.
    -

    -

    """ -@@ -2859,7 +2866,7 @@ Good luck picking a username that doesn't already exist.

    - - - -- -+ - -
    • This field is required.
    - """ -@@ -3489,7 +3496,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(), """

    Name:

    -
    Enter a valid email address.
    --

    Email:

    -+

    Email:

    -
    This field is required.
    -

    Comment:

    """) - -diff --git a/tests/validators/tests.py b/tests/validators/tests.py -index e39d0e3a1cef..1065727a974e 100644 ---- a/tests/validators/tests.py -+++ b/tests/validators/tests.py -@@ -59,6 +59,7 @@ TEST_DATA = [ - - (validate_email, 'example@atm.%s' % ('a' * 64), ValidationError), - (validate_email, 'example@%s.atm.%s' % ('b' * 64, 'a' * 63), ValidationError), -+ (validate_email, "example@%scom" % (("a" * 63 + ".") * 100), ValidationError), - (validate_email, None, ValidationError), - (validate_email, '', ValidationError), - (validate_email, 'abc', ValidationError), -@@ -246,6 +247,16 @@ TEST_DATA = [ - (URLValidator(), None, ValidationError), - (URLValidator(), 56, ValidationError), - (URLValidator(), 'no_scheme', ValidationError), -+ ( -+ URLValidator(), -+ "http://example." + ("a" * 63 + ".") * 1000 + "com", -+ ValidationError, -+ ), -+ ( -+ URLValidator(), -+ "http://userid:password" + "d" * 2000 + "@example.aaaaaaaaaaaaa.com", -+ None, -+ ), - # Newlines and tabs are not accepted. - (URLValidator(), 'http://www.djangoproject.com/\n', ValidationError), - (URLValidator(), 'http://[::ffff:192.9.5.5]\n', ValidationError), diff -Nru python-django-3.2.19/debian/patches/series python-django-3.2.19/debian/patches/series --- python-django-3.2.19/debian/patches/series 2023-07-28 13:24:04.000000000 +0000 +++ python-django-3.2.19/debian/patches/series 2024-08-21 11:08:24.000000000 +0000 @@ -10,4 +10,11 @@ 0011-Moved-RequestSite-import-to-the-toplevel.patch 0012-Add-Python-3.11-support-for-tests.patch 0013-fix-url-validator.patch -CVE-2023-36053.patch +0014-CVE-2023-36053.patch +0015-CVE-2024-39329.patch +0016-CVE-2024-39330.patch +0017-CVE-2024-39614-1.patch +0018-CVE-2024-39614-2.patch +0019-CVE-2024-41989.patch +0020-CVE-2024-41991.patch +0021-CVE-2024-42005.patch