Package: release.debian.org Severity: normal Tags: bookworm User: release.debian....@packages.debian.org Usertags: pu X-Debbugs-Cc: python-dja...@packages.debian.org, steve.mcint...@pexip.com Control: affects -1 + src:python-django
Hi! I've backported a lump of upstream CVE fixes for django to the version in bookworm. Chris Lamb has reviewed and approved the changes as one of the existing maintainers. The standard test suite all passes as expected. I've already uploaded to p-u Here's the changelog; debdiff attached. 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 <93...@debian.org> Wed, 21 Aug 2024 12:08:24 +0100
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 14:24:04.000000000 +0100 +++ python-django-3.2.19/debian/changelog 2024-08-21 12:08:24.000000000 +0100 @@ -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 <93...@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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0014-CVE-2023-36053.patch 2024-08-07 16:56:53.000000000 +0100 @@ -0,0 +1,242 @@ +From: Mariusz Felisiak <felisiak.mari...@gmail.com> +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, '<input type="email" name="f" id="id_f" required>') ++ self.assertEqual(f.max_length, 320) ++ self.assertWidgetRendersTo( ++ f, '<input type="email" name="f" id="id_f" maxlength="320" required>' ++ ) + 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']), '<input type="email" name="email" required>') ++ self.assertHTMLEqual( ++ str(f["email"]), ++ '<input type="email" name="email" maxlength="320" required>', ++ ) + self.assertHTMLEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" required>') + + f = SignupForm({'email': 't...@example.com', 'get_spam': True}, auto_id=False) +- self.assertHTMLEqual(str(f['email']), '<input type="email" name="email" value="t...@example.com" required>') ++ self.assertHTMLEqual( ++ str(f["email"]), ++ '<input type="email" name="email" maxlength="320" value="t...@example.com" ' ++ "required>", ++ ) + self.assertHTMLEqual( + str(f['get_spam']), + '<input checked type="checkbox" name="get_spam" required>', +@@ -2824,7 +2831,7 @@ Good luck picking a username that doesn't already exist.</p> + <option value="true">Yes</option> + <option value="false">No</option> + </select></li> +-<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email"></li> ++<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></li> + <li class="required error"><ul class="errorlist"><li>This field is required.</li></ul> + <label class="required" for="id_age">Age:</label> <input type="number" name="age" id="id_age" required></li>""" + ) +@@ -2840,7 +2847,7 @@ Good luck picking a username that doesn't already exist.</p> + <option value="true">Yes</option> + <option value="false">No</option> + </select></p> +-<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email"></p> ++<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></p> + <ul class="errorlist"><li>This field is required.</li></ul> + <p class="required error"><label class="required" for="id_age">Age:</label> + <input type="number" name="age" id="id_age" required></p>""" +@@ -2859,7 +2866,7 @@ Good luck picking a username that doesn't already exist.</p> + <option value="false">No</option> + </select></td></tr> + <tr><th><label for="id_email">Email:</label></th><td> +-<input type="email" name="email" id="id_email"></td></tr> ++<input type="email" name="email" id="id_email" maxlength="320"></td></tr> + <tr class="required error"><th><label class="required" for="id_age">Age:</label></th> + <td><ul class="errorlist"><li>This field is required.</li></ul> + <input type="number" name="age" id="id_age" required></td></tr>""" +@@ -3489,7 +3496,7 @@ Good luck picking a username that doesn't already exist.</p> + f = CommentForm(data, auto_id=False, error_class=DivErrorList) + self.assertHTMLEqual(f.as_p(), """<p>Name: <input type="text" name="name" maxlength="50"></p> + <div class="errorlist"><div class="error">Enter a valid email address.</div></div> +-<p>Email: <input type="email" name="email" value="invalid" required></p> ++<p>Email: <input type="email" name="email" value="invalid" maxlength="320" required></p> + <div class="errorlist"><div class="error">This field is required.</div></div> + <p>Comment: <input type="text" name="comment" required></p>""") + +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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0015-CVE-2024-39329.patch 2024-08-21 12:08:24.000000000 +0100 @@ -0,0 +1,80 @@ +commit 5d8645857936c142a3973694799c52165e2bdcdb +Author: Michael Manfre <m...@manfre.net> +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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0016-CVE-2024-39330.patch 2024-08-21 12:08:24.000000000 +0100 @@ -0,0 +1,145 @@ +commit fe4a0bbe2088d0c2b331216dad21ccd0bb3ee80d +Author: Natalia <124304+ness...@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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0017-CVE-2024-39614-1.patch 2024-08-21 12:08:24.000000000 +0100 @@ -0,0 +1,127 @@ +commit 9e9792228a6bb5d6402a5d645bc3be4cf364aefb +Author: Sarah Boyce <42296566+sarahbo...@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 + <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>. + """ + 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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0018-CVE-2024-39614-2.patch 2024-08-21 12:08:24.000000000 +0100 @@ -0,0 +1,92 @@ +commit 0e94f292cda632153f2b3d9a9037eb0141ae9c2e +Author: Lorenzo Peña <lorin...@gmail.com> +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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0019-CVE-2024-41989.patch 2024-08-21 12:08:24.000000000 +0100 @@ -0,0 +1,67 @@ +commit c19465ad87e33b6122c886b97a202ad54cd43672 +Author: Sarah Boyce <42296566+sarahbo...@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 <s...@platonix.com> + +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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0020-CVE-2024-41991.patch 2024-08-21 12:08:24.000000000 +0100 @@ -0,0 +1,108 @@ +commit 5f1757142febd95994caa1c0f64c1a0c161982c3 +Author: Mariusz Felisiak <felisiak.mari...@gmail.com> +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+sarahbo...@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 <a href="http://google.com/?q=">google.com/?q=</a>!' + ), + ('f...@example.com', '<a href="mailto:f...@example.com">f...@example.com</a>'), ++ ( ++ "test@" + "한.글." * 15 + "aaa", ++ '<a href="mailto:test@' ++ + "xn--6q8b.xn--bj0b." * 15 ++ + 'aaa">' ++ + "test@" ++ + "한.글." * 15 ++ + "aaa</a>", ++ ), + ) + 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 01:00:00.000000000 +0100 +++ python-django-3.2.19/debian/patches/0021-CVE-2024-42005.patch 2024-08-21 12:08:24.000000000 +0100 @@ -0,0 +1,70 @@ +commit c87bfaacf8fb84984243b5055dc70f97996cb115 +Author: Simon Charette <charett...@gmail.com> +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 14:24:04.000000000 +0100 +++ python-django-3.2.19/debian/patches/CVE-2023-36053.patch 1970-01-01 01:00:00.000000000 +0100 @@ -1,242 +0,0 @@ -From: Mariusz Felisiak <felisiak.mari...@gmail.com> -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, '<input type="email" name="f" id="id_f" required>') -+ self.assertEqual(f.max_length, 320) -+ self.assertWidgetRendersTo( -+ f, '<input type="email" name="f" id="id_f" maxlength="320" required>' -+ ) - 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']), '<input type="email" name="email" required>') -+ self.assertHTMLEqual( -+ str(f["email"]), -+ '<input type="email" name="email" maxlength="320" required>', -+ ) - self.assertHTMLEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" required>') - - f = SignupForm({'email': 't...@example.com', 'get_spam': True}, auto_id=False) -- self.assertHTMLEqual(str(f['email']), '<input type="email" name="email" value="t...@example.com" required>') -+ self.assertHTMLEqual( -+ str(f["email"]), -+ '<input type="email" name="email" maxlength="320" value="t...@example.com" ' -+ "required>", -+ ) - self.assertHTMLEqual( - str(f['get_spam']), - '<input checked type="checkbox" name="get_spam" required>', -@@ -2824,7 +2831,7 @@ Good luck picking a username that doesn't already exist.</p> - <option value="true">Yes</option> - <option value="false">No</option> - </select></li> --<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email"></li> -+<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></li> - <li class="required error"><ul class="errorlist"><li>This field is required.</li></ul> - <label class="required" for="id_age">Age:</label> <input type="number" name="age" id="id_age" required></li>""" - ) -@@ -2840,7 +2847,7 @@ Good luck picking a username that doesn't already exist.</p> - <option value="true">Yes</option> - <option value="false">No</option> - </select></p> --<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email"></p> -+<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" maxlength="320"></p> - <ul class="errorlist"><li>This field is required.</li></ul> - <p class="required error"><label class="required" for="id_age">Age:</label> - <input type="number" name="age" id="id_age" required></p>""" -@@ -2859,7 +2866,7 @@ Good luck picking a username that doesn't already exist.</p> - <option value="false">No</option> - </select></td></tr> - <tr><th><label for="id_email">Email:</label></th><td> --<input type="email" name="email" id="id_email"></td></tr> -+<input type="email" name="email" id="id_email" maxlength="320"></td></tr> - <tr class="required error"><th><label class="required" for="id_age">Age:</label></th> - <td><ul class="errorlist"><li>This field is required.</li></ul> - <input type="number" name="age" id="id_age" required></td></tr>""" -@@ -3489,7 +3496,7 @@ Good luck picking a username that doesn't already exist.</p> - f = CommentForm(data, auto_id=False, error_class=DivErrorList) - self.assertHTMLEqual(f.as_p(), """<p>Name: <input type="text" name="name" maxlength="50"></p> - <div class="errorlist"><div class="error">Enter a valid email address.</div></div> --<p>Email: <input type="email" name="email" value="invalid" required></p> -+<p>Email: <input type="email" name="email" value="invalid" maxlength="320" required></p> - <div class="errorlist"><div class="error">This field is required.</div></div> - <p>Comment: <input type="text" name="comment" required></p>""") - -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 14:24:04.000000000 +0100 +++ python-django-3.2.19/debian/patches/series 2024-08-21 12:08:24.000000000 +0100 @@ -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