Version in base suite: 3.2.19-1+deb12u1
Version in overlay suite: 3.2.19-1+deb12u2
Base version: python-django_3.2.19-1+deb12u2
Target version: python-django_3.2.25-0+deb12u1
Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-django/python-django_3.2.19-1+deb12u2.dsc
Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-django/python-django_3.2.25-0+deb12u1.dsc
Django.egg-info/PKG-INFO | 2
Django.egg-info/SOURCES.txt | 6
PKG-INFO | 2
debian/changelog | 183 ++++++++++++++--
debian/patches/0013-fix-url-validator.patch | 8
debian/patches/0014-CVE-2023-36053.patch | 242 ---------------------
debian/patches/0014-CVE-2024-39329.patch | 83 +++++++
debian/patches/0015-CVE-2024-39329.patch | 80 -------
debian/patches/0015-CVE-2024-39330.patch | 149 +++++++++++++
debian/patches/0016-CVE-2024-39330.patch | 145 ------------
debian/patches/0016-CVE-2024-39614.patch | 129 +++++++++++
debian/patches/0017-CVE-2024-39614-1.patch | 127 -----------
debian/patches/0017-CVE-2024-41989.patch | 76 ++++++
debian/patches/0018-CVE-2024-39614-2.patch | 92 --------
debian/patches/0018-CVE-2024-41991.patch | 113 ++++++++++
debian/patches/0019-CVE-2024-41989.patch | 67 -----
debian/patches/0019-CVE-2024-42005.patch | 74 ++++++
debian/patches/0020-CVE-2024-41991.patch | 108 ---------
debian/patches/0020-CVE-2024-45231.patch | 111 +++++++++
debian/patches/0021-CVE-2024-42005.patch | 70 ------
debian/patches/0021-CVE-2024-53907.patch | 85 +++++++
debian/patches/0022-CVE-2024-56374.patch | 258 +++++++++++++++++++++++
debian/patches/0023-CVE-2025-13372.patch | 92 ++++++++
debian/patches/0024-CVE-2025-26699.patch | 76 ++++++
debian/patches/0025-CVE-2025-32873.patch | 80 +++++++
debian/patches/0026-CVE-2025-48432.patch | 75 ++++++
debian/patches/0027-CVE-2025-48432-2.patch | 127 +++++++++++
debian/patches/0028-CVE-2025-57833.patch | 66 +++++
debian/patches/0029-CVE-2025-59681.patch | 166 ++++++++++++++
debian/patches/0030-CVE-2025-59682.patch | 66 +++++
debian/patches/0031-CVE-2025-64459.patch | 44 +++
debian/patches/0032-CVE-2025-64460.patch | 102 +++++++++
debian/patches/series | 27 +-
django/__init__.py | 2
django/contrib/auth/forms.py | 10
django/contrib/humanize/templatetags/humanize.py | 15 -
django/core/validators.py | 7
django/forms/fields.py | 3
django/utils/encoding.py | 6
django/utils/text.py | 75 ++++++
docs/_ext/djangodocs.py | 17 +
docs/ref/forms/fields.txt | 7
docs/ref/templates/builtins.txt | 20 +
docs/ref/utils.txt | 2
docs/ref/validators.txt | 25 ++
docs/releases/3.2.20.txt | 14 +
docs/releases/3.2.21.txt | 14 +
docs/releases/3.2.22.txt | 25 ++
docs/releases/3.2.23.txt | 19 +
docs/releases/3.2.24.txt | 13 +
docs/releases/3.2.25.txt | 22 +
docs/releases/index.txt | 6
docs/releases/security.txt | 66 +++++
tests/auth_tests/test_forms.py | 8
tests/forms_tests/field_tests/test_emailfield.py | 5
tests/forms_tests/field_tests/test_filefield.py | 9
tests/forms_tests/tests/test_forms.py | 19 +
tests/humanize_tests/tests.py | 152 ++++++++++++-
tests/requirements/py3.txt | 2
tests/utils_tests/test_encoding.py | 21 +
tests/utils_tests/test_text.py | 61 ++++-
tests/validators/tests.py | 11
62 files changed, 2771 insertions(+), 1016 deletions(-)
dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpj46_f67h/python-django_3.2.19-1+deb12u2.dsc: no acceptable signature found
dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpj46_f67h/python-django_3.2.25-0+deb12u1.dsc: no acceptable signature found
diff -Nru python-django-3.2.19/Django.egg-info/PKG-INFO python-django-3.2.25/Django.egg-info/PKG-INFO
--- python-django-3.2.19/Django.egg-info/PKG-INFO 2023-05-03 11:59:48.000000000 +0000
+++ python-django-3.2.25/Django.egg-info/PKG-INFO 2024-03-04 07:49:26.000000000 +0000
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: Django
-Version: 3.2.19
+Version: 3.2.25
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
Home-page: https://www.djangoproject.com/
Author: Django Software Foundation
diff -Nru python-django-3.2.19/Django.egg-info/SOURCES.txt python-django-3.2.25/Django.egg-info/SOURCES.txt
--- python-django-3.2.19/Django.egg-info/SOURCES.txt 2023-05-03 11:59:48.000000000 +0000
+++ python-django-3.2.25/Django.egg-info/SOURCES.txt 2024-03-04 07:49:26.000000000 +0000
@@ -4039,6 +4039,12 @@
docs/releases/3.2.18.txt
docs/releases/3.2.19.txt
docs/releases/3.2.2.txt
+docs/releases/3.2.20.txt
+docs/releases/3.2.21.txt
+docs/releases/3.2.22.txt
+docs/releases/3.2.23.txt
+docs/releases/3.2.24.txt
+docs/releases/3.2.25.txt
docs/releases/3.2.3.txt
docs/releases/3.2.4.txt
docs/releases/3.2.5.txt
diff -Nru python-django-3.2.19/PKG-INFO python-django-3.2.25/PKG-INFO
--- python-django-3.2.19/PKG-INFO 2023-05-03 11:59:49.717188000 +0000
+++ python-django-3.2.25/PKG-INFO 2024-03-04 07:49:27.267542600 +0000
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: Django
-Version: 3.2.19
+Version: 3.2.25
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
Home-page: https://www.djangoproject.com/
Author: Django Software Foundation
diff -Nru python-django-3.2.19/debian/changelog python-django-3.2.25/debian/changelog
--- python-django-3.2.19/debian/changelog 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/changelog 2026-01-27 19:16:59.000000000 +0000
@@ -1,25 +1,168 @@
-python-django (3:3.2.19-1+deb12u2) bookworm; urgency=high
+python-django (3:3.2.25-0+deb12u1) bookworm-security; 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.
+ * Update to upstream's last 3.2 series release:
- -- Steve McIntyre <93sam@debian.org> Wed, 21 Aug 2024 12:08:24 +0100
+ - CVE-2023-41164: Potential denial of service vulnerability in
+ django.utils.encoding.uri_to_iri().
+
+ This method was subject to potential denial of service attack via certain
+ inputs with a very large number of Unicode characters. This fix was
+ released in Django 3.2.21. (Closes: #1051226)
+
+ - CVE-2023-43665: Address a denial-of-service possibility in
+ django.utils.text.Truncator.
+
+ Following the fix for CVE-2019-14232, the regular expressions used in the
+ implementation of django.utils.text.Truncator’s chars() and words()
+ methods (with html=True) were revised and improved. However, these
+ regular expressions still exhibited linear backtracking complexity, so
+ when given a very long, potentially malformed HTML input, the evaluation
+ would still be slow, leading to a potential denial of service
+ vulnerability.
+
+ The chars() and words() methods are used to implement the
+ truncatechars_html and truncatewords_html template filters, which were
+ thus also vulnerable.
+
+ The input processed by Truncator, when operating in HTML mode, has been
+ limited to the first five million characters in order to avoid potential
+ performance and memory issues. This fix was included in Django 3.2.22.
+
+ - CVE-2024-24680: Potential denial-of-service in intcomma template filter.
+ The intcomma template filter was subject to a potential denial-of-service
+ attack when used with very long strings. This fix was included in Django
+ 3.2.24.
+
+ - CVE-2024-27351: Fix a potential regular expression denial-of-service
+ (ReDoS) attack in django.utils.text.Truncator.words. This method
+ (with html=True) and the truncatewords_html template filter were subject
+ to a potential regular expression denial-of-service attack via a suitably
+ crafted string. This is, in part, a follow up to CVE-2019-14232 and
+ CVE-2023-43665, and was included in Django 3.2.25.
+
+ * Drop debian/patches/CVE-2023-36053.patch now that we include the fix
+ directly via 3.2.20.
+
+ * CVE-2024-39329: Avoid a username enumeration vulnerability through timing
+ difference for users with unusable password. The authenticate method of
+ django.contrib.auth.backends.ModelBackend method allowed remote attackers
+ to enumerate users via a timing attack involving login requests for users
+ with unusable passwords.
+
+ * CVE-2024-39330: Address a potential directory-traversal in
+ django.core.files.storage.Storage.save. Derived classes of this method's
+ base class which override generate_filename without replicating the file
+ path validations existing in the parent class allowed for potential
+ directory-traversal via certain inputs when calling save(). Built-in
+ Storage sub-classes were not affected by this vulnerability.
+
+ * CVE-2024-39614: Fix a potential denial-of-service in
+ django.utils.translation.get_supported_language_variant. This method was
+ subject to a potential DoS attack when used with very long strings
+ containing specific characters. To mitigate this vulnerability, the
+ language code provided to get_supported_language_variant is now parsed up
+ to a maximum length of 500 characters.
+
+ * CVE-2024-41989: Memory exhaustion in django.utils.numberformat. The
+ floatformat template filter is subject to significant memory consumption
+ when given a string representation of a number in scientific notation with
+ a large exponent.
+
+ * CVE-2024-41991: Potential denial-of-service vulnerability in
+ django.utils.html.urlize() and AdminURLFieldWidget. The urlize and
+ urlizetrunc template filters, and the AdminURLFieldWidget widget, are
+ subject to a potential denial-of-service attack via certain inputs with a
+ very large number of Unicode characters.
+
+ * CVE-2024-42005: Potential SQL injection in QuerySet.values() and
+ values_list(). QuerySet.values() and values_list() methods on models with a
+ JSONField are subject to SQL injection in column aliases via a crafted JSON
+ object key as a passed *arg.
+
+ * CVE-2024-45231: Potential user email enumeration via response status on
+ password reset. Due to unhandled email sending failures, the
+ django.contrib.auth.forms.PasswordResetForm class allowed remote attackers
+ to enumerate user emails by issuing password reset requests and observing
+ the outcomes. To mitigate this risk, exceptions occurring during password
+ reset email sending are now handled and logged using the
+ django.contrib.auth logger.
+
+ * CVE-2024-53907: Potential DoS in django.utils.html.strip_tags.
+ The strip_tags() method and striptags template filter were subject to a
+ potential denial-of-service attack via certain inputs containing large
+ sequences of nested incomplete HTML entities.
+
+ * CVE-2024-56374: Potential denial-of-service vulnerability in IPv6
+ validation. A lack of upper bound limit enforcement in strings passed when
+ performing IPv6 validation could have led to a potential denial-of-service
+ (DoS) attack. The undocumented and private functions clean_ipv6_address and
+ is_valid_ipv6_address were vulnerable, as was the GenericIPAddressField
+ form field, which has now been updated to define a max_length of 39
+ characters. The GenericIPAddressField model field was not affected.
+
+ * CVE-2025-13372: Fix a potential SQL injection attack in FilteredRelation
+ column aliases when using PostgreSQL. FilteredRelation was subject to SQL
+ injection in column aliases via a suitably crafted dictionary as the
+ **kwargs passed to QuerySet.annotate() or QuerySet.alias().
+
+ * CVE-2025-26699: Address a potential denial-of-service in
+ django.utils.text.wrap. The wrap() method and wordwrap template filter were
+ subject to a potential denial-of-service attack when used with very long
+ strings. (Closes: #1099682)
+
+ * CVE-2025-32873: Denial-of-service possibility in strip_tags()
+ django.utils.html.strip_tags() would be slow to evaluate certain inputs
+ containing large sequences of incomplete HTML tags. This function is used
+ to implement the striptags template filter, which was therefore also
+ vulnerable. strip_tags() now raises a SuspiciousOperation exception if it
+ encounters an unusually large number of unclosed opening tags.
+
+ * CVE-2025-48432: Potential log injection via unescaped request path.
+ Django's internal HTTP response logging used request.path directly,
+ allowing control characters (e.g. newlines or ANSI escape sequences) to be
+ written unescaped into logs. This could enable log injection or forgery,
+ letting attackers manipulate log appearance or structure, especially in
+ logs processed by external systems or viewed in terminals. Although this
+ does not directly impact Django's security model, it poses risks when logs
+ are consumed or interpreted by other tools. To fix this, the internal
+ django.utils.log.log_response() function now escapes all positional
+ formatting arguments using a safe encoding.
+
+ * CVE-2025-57833: Potential SQL injection in FilteredRelation column
+ aliases. The FilteredRelation feature in Django was subject to a potential
+ SQL injection vulnerability in column aliases that was exploitable via
+ suitably crafted dictionary with dictionary expansion as the **kwargs
+ passed QuerySet.annotate() or QuerySet.alias(). (Closes: #1113865)
+
+ * CVE-2025-59681: Potential SQL injection in QuerySet.annotate(), alias(),
+ aggregate() and extra() on MySQL and MariaDB. QuerySet.annotate(),
+ QuerySet.alias(), QuerySet.aggregate() and QuerySet.extra() methods were
+ subject to SQL injection in column aliases, using a suitably crafted
+ dictionary with dictionary expansion as the **kwargs passed to these
+ methods on MySQL and MariaDB.
+
+ * CVE-2025-59682: Potential partial directory-traversal via
+ archive.extract(). The django.utils.archive.extract() function, used by
+ startapp --template and startproject --template allowed partial
+ directory-traversal via an archive with file paths sharing a common prefix
+ with the target directory.
+
+ * CVE-2025-64459: Prevent a potential SQL injection via _connector keyword
+ argument in QuerySet/Q objects. The methods QuerySet.filter(),
+ QuerySet.exclude(), and QuerySet.get() and the class Q() were subject to
+ SQL injection when using a suitably crafted dictionary (with dictionary
+ expansion) as the _connector argument.
+
+ * CVE-2025-64460: Prevent a potential denial-of-service vulnerability in
+ XML serializer text extraction. An algorithmic complexity issue in
+ django.core.serializers.xml_serializer.getInnerText() allowed a remote
+ attacker to cause a potential denial-of-service triggering CPU and memory
+ exhaustion via a specially crafted XML input submitted to a service that
+ invokes XML Deserializer. The vulnerability resulted from repeated string
+ concatenation while recursively collecting text nodes, which produced
+ superlinear computation.
+
+ -- Chris Lamb Tue, 27 Jan 2026 11:16:59 -0800
python-django (3:3.2.19-1+deb12u1) bookworm-security; urgency=high
diff -Nru python-django-3.2.19/debian/patches/0013-fix-url-validator.patch python-django-3.2.25/debian/patches/0013-fix-url-validator.patch
--- python-django-3.2.19/debian/patches/0013-fix-url-validator.patch 2024-08-07 15:56:53.000000000 +0000
+++ python-django-3.2.25/debian/patches/0013-fix-url-validator.patch 2026-01-27 19:16:59.000000000 +0000
@@ -10,10 +10,10 @@
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/django/core/validators.py b/django/core/validators.py
-index 731ccf2d4690..6b28eef08dd2 100644
+index b9b58dfa6176..52ebddac6345 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
-@@ -110,15 +110,16 @@ class URLValidator(RegexValidator):
+@@ -111,15 +111,16 @@ class URLValidator(RegexValidator):
raise ValidationError(self.message, code=self.code, params={'value': value})
# Then check full URL
@@ -34,7 +34,7 @@
try:
netloc = punycode(netloc) # IDN -> ACE
except UnicodeError: # invalid domain part
-@@ -129,7 +130,7 @@ class URLValidator(RegexValidator):
+@@ -130,7 +131,7 @@ class URLValidator(RegexValidator):
raise
else:
# Now verify IPv6 in the netloc part
@@ -43,7 +43,7 @@
if host_match:
potential_ip = host_match[1]
try:
-@@ -141,7 +142,7 @@ class URLValidator(RegexValidator):
+@@ -142,7 +143,7 @@ class URLValidator(RegexValidator):
# section 3.1. It's defined to be 255 bytes or less, but this includes
# one byte for the length of the name and one byte for the trailing dot
# that's used to indicate absolute names in DNS.
diff -Nru python-django-3.2.19/debian/patches/0014-CVE-2023-36053.patch python-django-3.2.25/debian/patches/0014-CVE-2023-36053.patch
--- python-django-3.2.19/debian/patches/0014-CVE-2023-36053.patch 2024-08-07 15:56:53.000000000 +0000
+++ python-django-3.2.25/debian/patches/0014-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.
- Yes
- No
-
--Email:
-+Email:
-
- Age: """
- )
-@@ -2840,7 +2847,7 @@ Good luck picking a username that doesn't already exist.
- Yes
- No
-
--Email:
-+Email:
-
- Age:
-
"""
-@@ -2859,7 +2866,7 @@ Good luck picking a username that doesn't already exist.
- No
-
- Email:
--
-+
- Age:
-
- """
-@@ -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:
-
- 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/0014-CVE-2024-39329.patch python-django-3.2.25/debian/patches/0014-CVE-2024-39329.patch
--- python-django-3.2.19/debian/patches/0014-CVE-2024-39329.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0014-CVE-2024-39329.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,83 @@
+From: Michael Manfre
+Date: Fri, 14 Jun 2024 22:12:58 -0400
+Subject: [PATCH] [4.2.x] 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.
+---
+ django/contrib/auth/hashers.py | 10 ++++++++--
+ tests/auth_tests/test_hashers.py | 32 ++++++++++++++++++++++++++++++++
+ 2 files changed, 40 insertions(+), 2 deletions(-)
+
+diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
+index 86ae7f42a8b1..ee81b641dca5 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 8bc61bc8b292..df47adea7805 100644
+--- a/tests/auth_tests/test_hashers.py
++++ b/tests/auth_tests/test_hashers.py
+@@ -272,6 +272,38 @@ class TestUtilsHashPass(SimpleTestCase):
+ expected_call = (('wrong_password', data[:29].encode()),)
+ self.assertEqual(hasher.encode.call_args_list, [expected_call] * 3)
+
++ 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)],
++ )
++
+ def test_unusable(self):
+ encoded = make_password(None)
+ self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
diff -Nru python-django-3.2.19/debian/patches/0015-CVE-2024-39329.patch python-django-3.2.25/debian/patches/0015-CVE-2024-39329.patch
--- python-django-3.2.19/debian/patches/0015-CVE-2024-39329.patch 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/0015-CVE-2024-39329.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,80 +0,0 @@
-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/0015-CVE-2024-39330.patch python-django-3.2.25/debian/patches/0015-CVE-2024-39330.patch
--- python-django-3.2.19/debian/patches/0015-CVE-2024-39330.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0015-CVE-2024-39330.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,149 @@
+From: Natalia <124304+nessita@users.noreply.github.com>
+Date: Wed, 20 Mar 2024 13:55:21 -0300
+Subject: [PATCH] [4.2.x] 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.
+---
+ django/core/files/storage.py | 11 +++++++
+ django/core/files/utils.py | 7 ++---
+ tests/file_storage/test_base.py | 70 +++++++++++++++++++++++++++++++++++++++++
+ tests/file_storage/tests.py | 6 ----
+ 4 files changed, 84 insertions(+), 10 deletions(-)
+ create mode 100644 tests/file_storage/test_base.py
+
+diff --git a/django/core/files/storage.py b/django/core/files/storage.py
+index 22984f9498d9..680f5ec91167 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 f28cea107758..a1fea44ded67 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 000000000000..c5338b8e668f
+--- /dev/null
++++ b/tests/file_storage/test_base.py
+@@ -0,0 +1,70 @@
++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 72380932447f..6d17a7118b4e 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/0016-CVE-2024-39330.patch python-django-3.2.25/debian/patches/0016-CVE-2024-39330.patch
--- python-django-3.2.19/debian/patches/0016-CVE-2024-39330.patch 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/0016-CVE-2024-39330.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,145 +0,0 @@
-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/0016-CVE-2024-39614.patch python-django-3.2.25/debian/patches/0016-CVE-2024-39614.patch
--- python-django-3.2.19/debian/patches/0016-CVE-2024-39614.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0016-CVE-2024-39614.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,129 @@
+From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
+Date: Wed, 26 Jun 2024 12:11:54 +0200
+Subject: [PATCH] [4.2.x] 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.
+---
+ django/utils/translation/trans_real.py | 23 ++++++++++++++++++-----
+ docs/ref/utils.txt | 10 ++++++++++
+ tests/i18n/tests.py | 11 +++++++++++
+ 3 files changed, 39 insertions(+), 5 deletions(-)
+
+diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
+index b262a5000a48..7c4bf095a94c 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,23 @@ 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:
++ index = lang_code.rfind("-", 0, LANGUAGE_CODE_MAX_LENGTH)
++ if not strict and index > 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 +608,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 0c296f8d55c4..fa97b98d6f3b 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 41ec63da99e4..793bd3db8402 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/0017-CVE-2024-39614-1.patch python-django-3.2.25/debian/patches/0017-CVE-2024-39614-1.patch
--- python-django-3.2.19/debian/patches/0017-CVE-2024-39614-1.patch 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/0017-CVE-2024-39614-1.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,127 +0,0 @@
-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/0017-CVE-2024-41989.patch python-django-3.2.25/debian/patches/0017-CVE-2024-41989.patch
--- python-django-3.2.19/debian/patches/0017-CVE-2024-41989.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0017-CVE-2024-41989.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,76 @@
+From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
+Date: Fri, 12 Jul 2024 11:38:34 +0200
+Subject: [PATCH] [4.2.x] Fixed CVE-2024-41989 -- Prevented excessive memory
+ consumption in floatformat. MIME-Version: 1.0 Content-Type: text/plain;
+ charset=UTF-8 Content-Transfer-Encoding: 8bit
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 8bit
+
+Thanks Elias Myllymäki for the report.
+
+Co-authored-by: Shai Berger
+---
+ django/template/defaultfilters.py | 13 +++++++++++++
+ tests/template_tests/filter_tests/test_floatformat.py | 17 +++++++++++++++++
+ 2 files changed, 30 insertions(+)
+
+diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
+index 92050122abdf..bc38d44b1d56 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 64d65f7dc48c..e65663ed2bd9 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):
+@@ -95,6 +96,22 @@ class FunctionTests(SimpleTestCase):
+ self.assertEqual(floatformat(pos_inf), 'inf')
+ self.assertEqual(floatformat(neg_inf), '-inf')
+ self.assertEqual(floatformat(pos_inf / pos_inf), 'nan')
++ self.assertEqual(floatformat("inf").replace("'", ""), "inf")
++ self.assertEqual(floatformat("NaN").replace("'", ""), "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).replace("'", ""), value)
+
+ def test_float_dunder_method(self):
+ class FloatWrapper:
diff -Nru python-django-3.2.19/debian/patches/0018-CVE-2024-39614-2.patch python-django-3.2.25/debian/patches/0018-CVE-2024-39614-2.patch
--- python-django-3.2.19/debian/patches/0018-CVE-2024-39614-2.patch 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/0018-CVE-2024-39614-2.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,92 +0,0 @@
-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/0018-CVE-2024-41991.patch python-django-3.2.25/debian/patches/0018-CVE-2024-41991.patch
--- python-django-3.2.19/debian/patches/0018-CVE-2024-41991.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0018-CVE-2024-41991.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,113 @@
+From: Mariusz Felisiak
+Date: Wed, 10 Jul 2024 20:30:12 +0200
+Subject: [PATCH] [4.2.x] 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>
+---
+ django/contrib/admin/widgets.py | 2 +-
+ django/utils/html.py | 10 ++++++++--
+ tests/admin_widgets/tests.py | 7 ++++++-
+ tests/utils_tests/test_html.py | 13 +++++++++++++
+ 4 files changed, 28 insertions(+), 4 deletions(-)
+
+diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
+index aeb74773ac64..b7dd0d87a0a6 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 3bc02b8dd3aa..44c6b7b81f4d 100644
+--- a/django/utils/html.py
++++ b/django/utils/html.py
+@@ -29,6 +29,8 @@ simple_url_2_re = _lazy_re_compile(
+ re.IGNORECASE
+ )
+
++MAX_URL_LENGTH = 2048
++
+
+ @keep_lazy(str, SafeString)
+ def escape(text):
+@@ -298,6 +300,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 f701f1abff83..ef6167374d77 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 30f5ba68e862..93458acd1f06 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/0019-CVE-2024-41989.patch python-django-3.2.25/debian/patches/0019-CVE-2024-41989.patch
--- python-django-3.2.19/debian/patches/0019-CVE-2024-41989.patch 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/0019-CVE-2024-41989.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,67 +0,0 @@
-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/0019-CVE-2024-42005.patch python-django-3.2.25/debian/patches/0019-CVE-2024-42005.patch
--- python-django-3.2.19/debian/patches/0019-CVE-2024-42005.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0019-CVE-2024-42005.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,74 @@
+From: Simon Charette
+Date: Thu, 25 Jul 2024 18:19:13 +0200
+Subject: [PATCH] [4.2.x] Fixed CVE-2024-42005 -- Mitigated QuerySet.values()
+ SQL injection attacks against JSON fields.
+
+Thanks Eyal (eyalgabay) for the report.
+---
+ django/db/models/sql/query.py | 2 ++
+ tests/expressions/models.py | 7 +++++++
+ tests/expressions/test_queryset_values.py | 17 +++++++++++++++--
+ 3 files changed, 24 insertions(+), 2 deletions(-)
+
+diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
+index 817e0bb7ffc7..56afcda64f8b 100644
+--- a/django/db/models/sql/query.py
++++ b/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 = []
+diff --git a/tests/expressions/models.py b/tests/expressions/models.py
+index bd6be81c5f16..01d1699d4879 100644
+--- a/tests/expressions/models.py
++++ b/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"}
+diff --git a/tests/expressions/test_queryset_values.py b/tests/expressions/test_queryset_values.py
+index 457a357d041b..2b524d8d99fe 100644
+--- a/tests/expressions/test_queryset_values.py
++++ b/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/0020-CVE-2024-41991.patch python-django-3.2.25/debian/patches/0020-CVE-2024-41991.patch
--- python-django-3.2.19/debian/patches/0020-CVE-2024-41991.patch 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/0020-CVE-2024-41991.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,108 +0,0 @@
-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/0020-CVE-2024-45231.patch python-django-3.2.25/debian/patches/0020-CVE-2024-45231.patch
--- python-django-3.2.19/debian/patches/0020-CVE-2024-45231.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0020-CVE-2024-45231.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,111 @@
+From: Natalia <124304+nessita@users.noreply.github.com>
+Date: Mon, 19 Aug 2024 14:47:38 -0300
+Subject: [PATCH] [4.2.x] Fixed CVE-2024-45231 -- Avoided server error on
+ password reset when email sending fails.
+
+On successful submission of a password reset request, an email is sent
+to the accounts known to the system. If sending this email fails (due to
+email backend misconfiguration, service provider outage, network issues,
+etc.), an attacker might exploit this by detecting which password reset
+requests succeed and which ones generate a 500 error response.
+
+Thanks to Thibaut Spriet for the report, and to Mariusz Felisiak, Adam
+Johnson, and Sarah Boyce for the reviews.
+---
+ django/contrib/auth/forms.py | 9 ++++++++-
+ docs/topics/auth/default.txt | 4 +++-
+ tests/auth_tests/test_forms.py | 21 +++++++++++++++++++++
+ tests/mail/custombackend.py | 5 +++++
+ 4 files changed, 37 insertions(+), 2 deletions(-)
+
+diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
+index fb7cfda2098d..85d82de9da15 100644
+--- a/django/contrib/auth/forms.py
++++ b/django/contrib/auth/forms.py
+@@ -1,3 +1,4 @@
++import logging
+ import unicodedata
+
+ from django import forms
+@@ -19,6 +20,7 @@ from django.utils.text import capfirst
+ from django.utils.translation import gettext, gettext_lazy as _
+
+ UserModel = get_user_model()
++logger = logging.getLogger("django.contrib.auth")
+
+
+ def _unicode_ci_compare(s1, s2):
+@@ -265,7 +267,12 @@ class PasswordResetForm(forms.Form):
+ html_email = loader.render_to_string(html_email_template_name, context)
+ email_message.attach_alternative(html_email, 'text/html')
+
+- email_message.send()
++ try:
++ email_message.send()
++ except Exception:
++ logger.exception(
++ "Failed to send password reset email to %s", context["user"].pk
++ )
+
+ def get_users(self, email):
+ """Given an email, return matching user(s) who should receive a reset.
+diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
+index 343c44084eb8..87bb98cbfb22 100644
+--- a/docs/topics/auth/default.txt
++++ b/docs/topics/auth/default.txt
+@@ -1526,7 +1526,9 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
+ .. method:: send_mail(subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None)
+
+ Uses the arguments to send an ``EmailMultiAlternatives``.
+- Can be overridden to customize how the email is sent to the user.
++ Can be overridden to customize how the email is sent to the user. If
++ you choose to override this method, be mindful of handling potential
++ exceptions raised due to email sending failures.
+
+ :param subject_template_name: the template for the subject.
+ :param email_template_name: the template for the email body.
+diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
+index c0e1975c1a98..86b7a5c88192 100644
+--- a/tests/auth_tests/test_forms.py
++++ b/tests/auth_tests/test_forms.py
+@@ -985,6 +985,27 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
+ message.get_payload(1).get_payload()
+ ))
+
++ @override_settings(EMAIL_BACKEND="mail.custombackend.FailingEmailBackend")
++ def test_save_send_email_exceptions_are_catched_and_logged(self):
++ (user, username, email) = self.create_dummy_user()
++ form = PasswordResetForm({"email": email})
++ self.assertTrue(form.is_valid())
++
++ with self.assertLogs("django.contrib.auth", level=0) as cm:
++ form.save()
++
++ self.assertEqual(len(mail.outbox), 0)
++ self.assertEqual(len(cm.output), 1)
++ errors = cm.output[0].split("\n")
++ pk = user.pk
++ self.assertEqual(
++ errors[0],
++ f"ERROR:django.contrib.auth:Failed to send password reset email to {pk}",
++ )
++ self.assertEqual(
++ errors[-1], "ValueError: FailingEmailBackend is doomed to fail."
++ )
++
+ @override_settings(AUTH_USER_MODEL='auth_tests.CustomEmailField')
+ def test_custom_email_field(self):
+ email = 'test@mail.com'
+diff --git a/tests/mail/custombackend.py b/tests/mail/custombackend.py
+index fd57777787c7..3e161d1b51a5 100644
+--- a/tests/mail/custombackend.py
++++ b/tests/mail/custombackend.py
+@@ -13,3 +13,8 @@ class EmailBackend(BaseEmailBackend):
+ # Messages are stored in an instance variable for testing.
+ self.test_outbox.extend(email_messages)
+ return len(email_messages)
++
++
++class FailingEmailBackend(BaseEmailBackend):
++ def send_messages(self, email_messages):
++ raise ValueError("FailingEmailBackend is doomed to fail.")
diff -Nru python-django-3.2.19/debian/patches/0021-CVE-2024-42005.patch python-django-3.2.25/debian/patches/0021-CVE-2024-42005.patch
--- python-django-3.2.19/debian/patches/0021-CVE-2024-42005.patch 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/0021-CVE-2024-42005.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,70 +0,0 @@
-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/0021-CVE-2024-53907.patch python-django-3.2.25/debian/patches/0021-CVE-2024-53907.patch
--- python-django-3.2.19/debian/patches/0021-CVE-2024-53907.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0021-CVE-2024-53907.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,85 @@
+From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
+Date: Wed, 13 Nov 2024 15:06:23 +0100
+Subject: [PATCH] [4.2.x] Fixed CVE-2024-53907 -- Mitigated potential DoS in
+ strip_tags().
+
+Thanks to jiangniao for the report, and Shai Berger and Natalia Bidart
+for the reviews.
+---
+ django/utils/html.py | 10 ++++++++--
+ tests/utils_tests/test_html.py | 7 +++++++
+ 2 files changed, 15 insertions(+), 2 deletions(-)
+
+diff --git a/django/utils/html.py b/django/utils/html.py
+index 44c6b7b81f4d..5887bf1b4d7d 100644
+--- a/django/utils/html.py
++++ b/django/utils/html.py
+@@ -8,6 +8,7 @@ from urllib.parse import (
+ parse_qsl, quote, unquote, urlencode, urlsplit, urlunsplit,
+ )
+
++from django.core.exceptions import SuspiciousOperation
+ from django.utils.encoding import punycode
+ from django.utils.functional import Promise, keep_lazy, keep_lazy_text
+ from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS
+@@ -30,6 +31,7 @@ simple_url_2_re = _lazy_re_compile(
+ )
+
+ MAX_URL_LENGTH = 2048
++MAX_STRIP_TAGS_DEPTH = 50
+
+
+ @keep_lazy(str, SafeString)
+@@ -181,15 +183,19 @@ def _strip_once(value):
+ @keep_lazy_text
+ def strip_tags(value):
+ """Return the given HTML with all tags stripped."""
+- # Note: in typical case this loop executes _strip_once once. Loop condition
+- # is redundant, but helps to reduce number of executions of _strip_once.
+ value = str(value)
++ # Note: in typical case this loop executes _strip_once twice (the second
++ # execution does not remove any more tags).
++ strip_tags_depth = 0
+ while '<' in value and '>' in value:
++ if strip_tags_depth >= MAX_STRIP_TAGS_DEPTH:
++ raise SuspiciousOperation
+ new_value = _strip_once(value)
+ if value.count('<') == new_value.count('<'):
+ # _strip_once wasn't able to detect more tags.
+ break
+ value = new_value
++ strip_tags_depth += 1
+ return value
+
+
+diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
+index 93458acd1f06..231f5d8f96bc 100644
+--- a/tests/utils_tests/test_html.py
++++ b/tests/utils_tests/test_html.py
+@@ -1,6 +1,7 @@
+ import os
+ from datetime import datetime
+
++from django.core.exceptions import SuspiciousOperation
+ from django.test import SimpleTestCase
+ from django.utils.functional import lazystr
+ from django.utils.html import (
+@@ -92,12 +93,18 @@ class TestUtilsHtml(SimpleTestCase):
+ ('&h', 'alert()h'),
+ ('>br>br>br>X', 'XX'),
++ ("<" * 50 + "a>" * 50, ""),
+ )
+ for value, output in items:
+ with self.subTest(value=value, output=output):
+ self.check_output(strip_tags, value, output)
+ self.check_output(strip_tags, lazystr(value), output)
+
++ def test_strip_tags_suspicious_operation(self):
++ value = "<" * 51 + "a>" * 51, ""
++ with self.assertRaises(SuspiciousOperation):
++ strip_tags(value)
++
+ def test_strip_tags_files(self):
+ # Test with more lengthy content (also catching performance regressions)
+ for filename in ('strip_tags1.html', 'strip_tags2.txt'):
diff -Nru python-django-3.2.19/debian/patches/0022-CVE-2024-56374.patch python-django-3.2.25/debian/patches/0022-CVE-2024-56374.patch
--- python-django-3.2.19/debian/patches/0022-CVE-2024-56374.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0022-CVE-2024-56374.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,258 @@
+From: Natalia <124304+nessita@users.noreply.github.com>
+Date: Mon, 6 Jan 2025 15:51:45 -0300
+Subject: [PATCH] [4.2.x] Fixed CVE-2024-56374 -- Mitigated potential DoS in
+ IPv6 validation.
+
+Thanks Saravana Kumar for the report, and Sarah Boyce and Mariusz
+Felisiak for the reviews.
+
+Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
+---
+ django/db/models/fields/__init__.py | 6 ++--
+ django/forms/fields.py | 7 ++--
+ django/utils/ipv6.py | 16 +++++++--
+ docs/ref/forms/fields.txt | 4 +--
+ .../field_tests/test_genericipaddressfield.py | 32 ++++++++++++++++-
+ tests/utils_tests/test_ipv6.py | 40 ++++++++++++++++++++--
+ 6 files changed, 91 insertions(+), 14 deletions(-)
+
+diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
+index 167c3d2f030f..5b439601a5cb 100644
+--- a/django/db/models/fields/__init__.py
++++ b/django/db/models/fields/__init__.py
+@@ -22,7 +22,7 @@ from django.utils.dateparse import (
+ )
+ from django.utils.duration import duration_microseconds, duration_string
+ from django.utils.functional import Promise, cached_property
+-from django.utils.ipv6 import clean_ipv6_address
++from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address
+ from django.utils.itercompat import is_iterable
+ from django.utils.text import capfirst
+ from django.utils.translation import gettext_lazy as _
+@@ -1913,7 +1913,7 @@ class GenericIPAddressField(Field):
+ self.default_validators, invalid_error_message = \
+ validators.ip_address_validators(protocol, unpack_ipv4)
+ self.default_error_messages['invalid'] = invalid_error_message
+- kwargs['max_length'] = 39
++ kwargs['max_length'] = MAX_IPV6_ADDRESS_LENGTH
+ super().__init__(verbose_name, name, *args, **kwargs)
+
+ def check(self, **kwargs):
+@@ -1940,7 +1940,7 @@ class GenericIPAddressField(Field):
+ kwargs['unpack_ipv4'] = self.unpack_ipv4
+ if self.protocol != "both":
+ kwargs['protocol'] = self.protocol
+- if kwargs.get("max_length") == 39:
++ if kwargs.get("max_length") == MAX_IPV6_ADDRESS_LENGTH:
+ del kwargs['max_length']
+ return name, path, args, kwargs
+
+diff --git a/django/forms/fields.py b/django/forms/fields.py
+index 8adb09e38294..6969c4a9d051 100644
+--- a/django/forms/fields.py
++++ b/django/forms/fields.py
+@@ -28,7 +28,7 @@ from django.forms.widgets import (
+ from django.utils import formats
+ from django.utils.dateparse import parse_datetime, parse_duration
+ from django.utils.duration import duration_string
+-from django.utils.ipv6 import clean_ipv6_address
++from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address
+ from django.utils.regex_helper import _lazy_re_compile
+ from django.utils.translation import gettext_lazy as _, ngettext_lazy
+
+@@ -1179,6 +1179,7 @@ class GenericIPAddressField(CharField):
+ def __init__(self, *, protocol='both', unpack_ipv4=False, **kwargs):
+ self.unpack_ipv4 = unpack_ipv4
+ self.default_validators = validators.ip_address_validators(protocol, unpack_ipv4)[0]
++ kwargs.setdefault("max_length", MAX_IPV6_ADDRESS_LENGTH)
+ super().__init__(**kwargs)
+
+ def to_python(self, value):
+@@ -1186,7 +1187,9 @@ class GenericIPAddressField(CharField):
+ return ''
+ value = value.strip()
+ if value and ':' in value:
+- return clean_ipv6_address(value, self.unpack_ipv4)
++ return clean_ipv6_address(
++ value, self.unpack_ipv4, max_length=self.max_length
++ )
+ return value
+
+
+diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py
+index ddb8c8091d2f..ef88f5263659 100644
+--- a/django/utils/ipv6.py
++++ b/django/utils/ipv6.py
+@@ -3,9 +3,19 @@ import ipaddress
+ from django.core.exceptions import ValidationError
+ from django.utils.translation import gettext_lazy as _
+
++MAX_IPV6_ADDRESS_LENGTH = 39
++
++
++def _ipv6_address_from_str(ip_str, max_length=MAX_IPV6_ADDRESS_LENGTH):
++ if len(ip_str) > max_length:
++ raise ValueError(
++ f"Unable to convert {ip_str} to an IPv6 address (value too long)."
++ )
++ return ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str)))
++
+
+ def clean_ipv6_address(ip_str, unpack_ipv4=False,
+- error_message=_("This is not a valid IPv6 address.")):
++ error_message=_("This is not a valid IPv6 address."), max_length=MAX_IPV6_ADDRESS_LENGTH):
+ """
+ Clean an IPv6 address string.
+
+@@ -23,7 +33,7 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False,
+ Return a compressed IPv6 address or the same value.
+ """
+ try:
+- addr = ipaddress.IPv6Address(int(ipaddress.IPv6Address(ip_str)))
++ addr = _ipv6_address_from_str(ip_str, max_length)
+ except ValueError:
+ raise ValidationError(error_message, code='invalid')
+
+@@ -40,7 +50,7 @@ def is_valid_ipv6_address(ip_str):
+ Return whether or not the `ip_str` string is a valid IPv6 address.
+ """
+ try:
+- ipaddress.IPv6Address(ip_str)
++ _ipv6_address_from_str(ip_str)
+ except ValueError:
+ return False
+ return True
+diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
+index 5b485f215384..9e0915894b5d 100644
+--- a/docs/ref/forms/fields.txt
++++ b/docs/ref/forms/fields.txt
+@@ -847,7 +847,7 @@ For each field, we describe the default widget used if you don't specify
+ * Empty value: ``''`` (an empty string)
+ * Normalizes to: A string. IPv6 addresses are normalized as described below.
+ * Validates that the given value is a valid IP address.
+- * Error message keys: ``required``, ``invalid``
++ * Error message keys: ``required``, ``invalid``, ``max_length``
+
+ The IPv6 address normalization follows :rfc:`4291#section-2.2` section 2.2,
+ including using the IPv4 format suggested in paragraph 3 of that section, like
+@@ -855,7 +855,7 @@ For each field, we describe the default widget used if you don't specify
+ ``2001::1``, and ``::ffff:0a0a:0a0a`` to ``::ffff:10.10.10.10``. All characters
+ are converted to lowercase.
+
+- Takes two optional arguments:
++ Takes three optional arguments:
+
+ .. attribute:: protocol
+
+diff --git a/tests/forms_tests/field_tests/test_genericipaddressfield.py b/tests/forms_tests/field_tests/test_genericipaddressfield.py
+index 92dbd71a284b..88eddb2448bf 100644
+--- a/tests/forms_tests/field_tests/test_genericipaddressfield.py
++++ b/tests/forms_tests/field_tests/test_genericipaddressfield.py
+@@ -1,6 +1,7 @@
+ from django.core.exceptions import ValidationError
+ from django.forms import GenericIPAddressField
+ from django.test import SimpleTestCase
++from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH
+
+
+ class GenericIPAddressFieldTest(SimpleTestCase):
+@@ -90,6 +91,35 @@ class GenericIPAddressFieldTest(SimpleTestCase):
+ with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"):
+ f.clean('1:2')
+
++ def test_generic_ipaddress_max_length_custom(self):
++ # Valid IPv4-mapped IPv6 address, len 45.
++ addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228"
++ f = GenericIPAddressField(max_length=len(addr))
++ f.clean(addr)
++
++ def test_generic_ipaddress_max_length_validation_error(self):
++ # Valid IPv4-mapped IPv6 address, len 45.
++ addr = "0000:0000:0000:0000:0000:ffff:192.168.100.228"
++
++ cases = [
++ ({}, MAX_IPV6_ADDRESS_LENGTH), # Default value.
++ ({"max_length": len(addr) - 1}, len(addr) - 1),
++ ]
++ for kwargs, max_length in cases:
++ max_length_plus_one = max_length + 1
++ msg = (
++ f"Ensure this value has at most {max_length} characters (it has "
++ f"{max_length_plus_one}).'"
++ )
++ with self.subTest(max_length=max_length):
++ f = GenericIPAddressField(**kwargs)
++ with self.assertRaisesMessage(ValidationError, msg):
++ f.clean("x" * max_length_plus_one)
++ with self.assertRaisesMessage(
++ ValidationError, "This is not a valid IPv6 address."
++ ):
++ f.clean(addr)
++
+ def test_generic_ipaddress_as_generic_not_required(self):
+ f = GenericIPAddressField(required=False)
+ self.assertEqual(f.clean(''), '')
+@@ -104,7 +134,7 @@ class GenericIPAddressFieldTest(SimpleTestCase):
+ with self.assertRaisesMessage(ValidationError, "'Enter a valid IPv4 or IPv6 address.'"):
+ f.clean('256.125.1.5')
+ self.assertEqual(f.clean(' fe80::223:6cff:fe8a:2e8a '), 'fe80::223:6cff:fe8a:2e8a')
+- self.assertEqual(f.clean(' 2a02::223:6cff:fe8a:2e8a '), '2a02::223:6cff:fe8a:2e8a')
++ self.assertEqual(f.clean(" " * MAX_IPV6_ADDRESS_LENGTH + ' 2a02::223:6cff:fe8a:2e8a '), '2a02::223:6cff:fe8a:2e8a')
+ with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"):
+ f.clean('12345:2:3:4')
+ with self.assertRaisesMessage(ValidationError, "'This is not a valid IPv6 address.'"):
+diff --git a/tests/utils_tests/test_ipv6.py b/tests/utils_tests/test_ipv6.py
+index 4e434f3c3aa0..1ac6763d9b93 100644
+--- a/tests/utils_tests/test_ipv6.py
++++ b/tests/utils_tests/test_ipv6.py
+@@ -1,9 +1,17 @@
+-import unittest
++import traceback
++from io import StringIO
+
+-from django.utils.ipv6 import clean_ipv6_address, is_valid_ipv6_address
++from django.core.exceptions import ValidationError
++from django.test import SimpleTestCase
++from django.utils.ipv6 import (
++ MAX_IPV6_ADDRESS_LENGTH,
++ clean_ipv6_address,
++ is_valid_ipv6_address,
++)
++from django.utils.version import PY310
+
+
+-class TestUtilsIPv6(unittest.TestCase):
++class TestUtilsIPv6(SimpleTestCase):
+
+ def test_validates_correct_plain_address(self):
+ self.assertTrue(is_valid_ipv6_address('fe80::223:6cff:fe8a:2e8a'))
+@@ -55,3 +63,29 @@ class TestUtilsIPv6(unittest.TestCase):
+ self.assertEqual(clean_ipv6_address('::ffff:0a0a:0a0a', unpack_ipv4=True), '10.10.10.10')
+ self.assertEqual(clean_ipv6_address('::ffff:1234:1234', unpack_ipv4=True), '18.52.18.52')
+ self.assertEqual(clean_ipv6_address('::ffff:18.52.18.52', unpack_ipv4=True), '18.52.18.52')
++
++ def test_address_too_long(self):
++ addresses = [
++ "0000:0000:0000:0000:0000:ffff:192.168.100.228", # IPv4-mapped IPv6 address
++ "0000:0000:0000:0000:0000:ffff:192.168.100.228%123456", # % scope/zone
++ "fe80::223:6cff:fe8a:2e8a:1234:5678:00000", # MAX_IPV6_ADDRESS_LENGTH + 1
++ ]
++ msg = "This is the error message."
++ value_error_msg = "Unable to convert %s to an IPv6 address (value too long)."
++ for addr in addresses:
++ with self.subTest(addr=addr):
++ self.assertGreater(len(addr), MAX_IPV6_ADDRESS_LENGTH)
++ self.assertEqual(is_valid_ipv6_address(addr), False)
++ with self.assertRaisesMessage(ValidationError, msg) as ctx:
++ clean_ipv6_address(addr, error_message=msg)
++ exception_traceback = StringIO()
++ if PY310:
++ traceback.print_exception(ctx.exception, file=exception_traceback)
++ else:
++ traceback.print_exception(
++ type(ctx.exception),
++ value=ctx.exception,
++ tb=ctx.exception.__traceback__,
++ file=exception_traceback,
++ )
++ self.assertIn(value_error_msg % addr, exception_traceback.getvalue())
diff -Nru python-django-3.2.19/debian/patches/0023-CVE-2025-13372.patch python-django-3.2.25/debian/patches/0023-CVE-2025-13372.patch
--- python-django-3.2.19/debian/patches/0023-CVE-2025-13372.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0023-CVE-2025-13372.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,92 @@
+From: Jacob Walls
+Date: Mon, 17 Nov 2025 17:09:54 -0500
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-13372 -- Protected FilteredRelation
+ against SQL injection in column aliases on PostgreSQL.
+
+Follow-up to CVE-2025-57833.
+
+Thanks Stackered for the report, and Simon Charette and Mariusz Felisiak
+for the reviews.
+
+Backport of 5b90ca1e7591fa36fccf2d6dad67cf1477e6293e from main.
+---
+ django/db/backends/postgresql/compiler.py | 24 ++++++++++++++++++++++++
+ django/db/backends/postgresql/operations.py | 1 +
+ tests/annotations/tests.py | 13 +++++++++++++
+ 3 files changed, 38 insertions(+)
+ create mode 100644 django/db/backends/postgresql/compiler.py
+
+diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py
+new file mode 100644
+index 000000000000..d4140c7f98a6
+--- /dev/null
++++ b/django/db/backends/postgresql/compiler.py
+@@ -0,0 +1,24 @@
++from django.db.models.sql.compiler import ( # isort:skip
++ SQLAggregateCompiler,
++ SQLCompiler as BaseSQLCompiler,
++ SQLDeleteCompiler,
++ SQLInsertCompiler,
++ SQLUpdateCompiler,
++)
++
++__all__ = [
++ "SQLAggregateCompiler",
++ "SQLCompiler",
++ "SQLDeleteCompiler",
++ "SQLInsertCompiler",
++ "SQLUpdateCompiler",
++]
++
++
++class SQLCompiler(BaseSQLCompiler):
++ def quote_name_unless_alias(self, name):
++ if "$" in name:
++ raise ValueError(
++ "Dollar signs are not permitted in column aliases on PostgreSQL."
++ )
++ return super().quote_name_unless_alias(name)
+diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py
+index 0ef622efa869..1ffc9852e34d 100644
+--- a/django/db/backends/postgresql/operations.py
++++ b/django/db/backends/postgresql/operations.py
+@@ -5,6 +5,7 @@ from django.db.backends.base.operations import BaseDatabaseOperations
+
+
+ class DatabaseOperations(BaseDatabaseOperations):
++ compiler_module = "django.db.backends.postgresql.compiler"
+ cast_char_field_without_max_length = 'varchar'
+ explain_prefix = 'EXPLAIN'
+ explain_options = frozenset(
+diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
+index 8082c7af7585..70dd793a6c7c 100644
+--- a/tests/annotations/tests.py
++++ b/tests/annotations/tests.py
+@@ -2,10 +2,12 @@ import datetime
+ from decimal import Decimal
+
+ from django.core.exceptions import FieldDoesNotExist, FieldError
++from django.db import connection
+ from django.db.models import (
+ BooleanField, Case, CharField, Count, DateTimeField, DecimalField, Exists,
+ ExpressionWrapper, F, FloatField, Func, IntegerField, Max,
+ NullBooleanField, OuterRef, Q, Subquery, Sum, Value, When,
++ FilteredRelation,
+ )
+ from django.db.models.expressions import RawSQL
+ from django.db.models.functions import (
+@@ -1039,3 +1041,14 @@ class AliasTests(TestCase):
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ Book.objects.alias(**{crafted_alias: Value(1)})
++
++ def test_alias_filtered_relation_sql_injection_dollar_sign(self):
++ qs = Book.objects.alias(
++ **{"crafted_alia$": FilteredRelation("authors")}
++ ).values("name", "crafted_alia$")
++ if connection.vendor == "postgresql":
++ msg = "Dollar signs are not permitted in column aliases on PostgreSQL."
++ with self.assertRaisesMessage(ValueError, msg):
++ list(qs)
++ else:
++ self.assertEqual(qs.first()["name"], self.b1.name)
diff -Nru python-django-3.2.19/debian/patches/0024-CVE-2025-26699.patch python-django-3.2.25/debian/patches/0024-CVE-2025-26699.patch
--- python-django-3.2.19/debian/patches/0024-CVE-2025-26699.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0024-CVE-2025-26699.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,76 @@
+From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
+Date: Tue, 25 Feb 2025 09:40:54 +0100
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-26699 -- Mitigated potential DoS in
+ wordwrap template filter.
+
+Thanks sw0rd1ight for the report.
+
+Backport of 55d89e25f4115c5674cdd9b9bcba2bb2bb6d820b from main.
+---
+ django/utils/text.py | 27 ++++++++--------------
+ tests/template_tests/filter_tests/test_wordwrap.py | 11 +++++++++
+ 2 files changed, 21 insertions(+), 17 deletions(-)
+
+diff --git a/django/utils/text.py b/django/utils/text.py
+index 88da9a2c2c6b..cabd76f33f82 100644
+--- a/django/utils/text.py
++++ b/django/utils/text.py
+@@ -1,5 +1,6 @@
+ import html.entities
+ import re
++import textwrap
+ import unicodedata
+ import warnings
+ from gzip import GzipFile
+@@ -91,23 +92,15 @@ def wrap(text, width):
+ Don't wrap long words, thus the output text may have lines longer than
+ ``width``.
+ """
+- def _generator():
+- for line in text.splitlines(True): # True keeps trailing linebreaks
+- max_width = min((line.endswith('\n') and width + 1 or width), width)
+- while len(line) > max_width:
+- space = line[:max_width + 1].rfind(' ') + 1
+- if space == 0:
+- space = line.find(' ') + 1
+- if space == 0:
+- yield line
+- line = ''
+- break
+- yield '%s\n' % line[:space - 1]
+- line = line[space:]
+- max_width = min((line.endswith('\n') and width + 1 or width), width)
+- if line:
+- yield line
+- return ''.join(_generator())
++ wrapper = textwrap.TextWrapper(
++ width=width,
++ break_long_words=False,
++ break_on_hyphens=False,
++ )
++ result = []
++ for line in text.splitlines(True):
++ result.extend(wrapper.wrap(line))
++ return "\n".join(result)
+
+
+ class Truncator(SimpleLazyObject):
+diff --git a/tests/template_tests/filter_tests/test_wordwrap.py b/tests/template_tests/filter_tests/test_wordwrap.py
+index 02f860582ba7..f61842cb19aa 100644
+--- a/tests/template_tests/filter_tests/test_wordwrap.py
++++ b/tests/template_tests/filter_tests/test_wordwrap.py
+@@ -51,3 +51,14 @@ class FunctionTests(SimpleTestCase):
+ ), 14),
+ 'this is a long\nparagraph of\ntext that\nreally needs\nto be wrapped\nI\'m afraid',
+ )
++
++ def test_wrap_long_text(self):
++ long_text = (
++ "this is a long paragraph of text that really needs"
++ " to be wrapped I'm afraid " * 20_000
++ )
++ self.assertIn(
++ "this is a\nlong\nparagraph\nof text\nthat\nreally\nneeds to\nbe wrapped\n"
++ "I'm afraid",
++ wordwrap(long_text, 10),
++ )
diff -Nru python-django-3.2.19/debian/patches/0025-CVE-2025-32873.patch python-django-3.2.25/debian/patches/0025-CVE-2025-32873.patch
--- python-django-3.2.19/debian/patches/0025-CVE-2025-32873.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0025-CVE-2025-32873.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,80 @@
+From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
+Date: Tue, 8 Apr 2025 16:30:17 +0200
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-32873 -- Mitigated potential DoS in
+ strip_tags(). MIME-Version: 1.0 Content-Type: text/plain;
+ charset=UTF-8 Content-Transfer-Encoding: 8bit
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 8bit
+
+Thanks to Elias Myllymäki for the report, and Shai Berger and Jake
+Howard for the reviews.
+
+Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
+
+Backport of 9f3419b519799d69f2aba70b9d25abe2e70d03e0 from main.
+---
+ django/utils/html.py | 6 ++++++
+ tests/utils_tests/test_html.py | 15 ++++++++++++++-
+ 2 files changed, 20 insertions(+), 1 deletion(-)
+
+diff --git a/django/utils/html.py b/django/utils/html.py
+index 5887bf1b4d7d..1a3033e697b3 100644
+--- a/django/utils/html.py
++++ b/django/utils/html.py
+@@ -33,6 +33,9 @@ simple_url_2_re = _lazy_re_compile(
+ MAX_URL_LENGTH = 2048
+ MAX_STRIP_TAGS_DEPTH = 50
+
++# HTML tag that opens but has no closing ">" after 1k+ chars.
++long_open_tag_without_closing_re = _lazy_re_compile(r"<[a-zA-Z][^>]{1000,}")
++
+
+ @keep_lazy(str, SafeString)
+ def escape(text):
+@@ -184,6 +187,9 @@ def _strip_once(value):
+ def strip_tags(value):
+ """Return the given HTML with all tags stripped."""
+ value = str(value)
++ for long_open_tag in long_open_tag_without_closing_re.finditer(value):
++ if long_open_tag.group().count("<") >= MAX_STRIP_TAGS_DEPTH:
++ raise SuspiciousOperation
+ # Note: in typical case this loop executes _strip_once twice (the second
+ # execution does not remove any more tags).
+ strip_tags_depth = 0
+diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
+index 231f5d8f96bc..5c2de01fe234 100644
+--- a/tests/utils_tests/test_html.py
++++ b/tests/utils_tests/test_html.py
+@@ -94,17 +94,30 @@ class TestUtilsHtml(SimpleTestCase):
+ ('>br>br>br>X', 'XX'),
+ ("<" * 50 + "a>" * 50, ""),
++ (">" + "" + " " * 51, " "
+ with self.assertRaises(SuspiciousOperation):
+ strip_tags(value)
+
++ def test_strip_tags_suspicious_operation_large_open_tags(self):
++ items = [
++ ">" + "
+Date: Tue, 20 May 2025 15:29:52 -0300
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-48432 -- Escaped formatting arguments
+ in `log_response()`.
+
+Suitably crafted requests containing a CRLF sequence in the request
+path may have allowed log injection, potentially corrupting log files,
+obscuring other attacks, misleading log post-processing tools, or
+forging log entries.
+
+To mitigate this, all positional formatting arguments passed to the
+logger are now escaped using "unicode_escape" encoding.
+
+Thanks to Seokchan Yoon (https://ch4n3.kr/) for the report.
+
+Co-authored-by: Carlton Gibson
+Co-authored-by: Jake Howard
+
+Backport of a07ebec5591e233d8bbb38b7d63f35c5479eef0e from main.
+---
+ django/utils/log.py | 7 ++++++-
+ tests/logging_tests/tests.py | 14 ++++++++++++++
+ 2 files changed, 20 insertions(+), 1 deletion(-)
+
+diff --git a/django/utils/log.py b/django/utils/log.py
+index 3d3e8701c7b9..6d5c812d3284 100644
+--- a/django/utils/log.py
++++ b/django/utils/log.py
+@@ -221,8 +221,13 @@ def log_response(message, *args, response=None, request=None, logger=request_log
+ else:
+ level = 'info'
+
++ escaped_args = tuple(
++ a.encode("unicode_escape").decode("ascii") if isinstance(a, str) else a
++ for a in args
++ )
++
+ getattr(logger, level)(
+- message, *args,
++ message, *escaped_args,
+ extra={
+ 'status_code': response.status_code,
+ 'request': request,
+diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py
+index e5659057952f..5da64c76d88f 100644
+--- a/tests/logging_tests/tests.py
++++ b/tests/logging_tests/tests.py
+@@ -122,6 +122,14 @@ class HandlerLoggingTests(SetupDefaultLoggingMixin, LoggingAssertionMixin, Loggi
+ self.client.get('/redirect/')
+ self.assertEqual(self.logger_output.getvalue(), '')
+
++ def test_control_chars_escaped(self):
++ self.assertLogsRequest(
++ url="/%1B[1;31mNOW IN RED!!!1B[0m/",
++ level="WARNING",
++ status_code=404,
++ msg=r"Not Found: /\x1b[1;31mNOW IN RED!!!1B[0m/",
++ )
++
+ def test_page_not_found_warning(self):
+ self.assertLogsRequest(
+ url='/does_not_exist/',
+@@ -130,6 +138,12 @@ class HandlerLoggingTests(SetupDefaultLoggingMixin, LoggingAssertionMixin, Loggi
+ msg='Not Found: /does_not_exist/',
+ )
+
++ async def test_async_control_chars_escaped(self):
++ logger = "django.request"
++ level = "WARNING"
++ with self.assertLogs(logger, level) as cm:
++ await self.async_client.get(r"/%1B[1;31mNOW IN RED!!!1B[0m/")
++
+ def test_page_not_found_raised(self):
+ self.assertLogsRequest(
+ url='/does_not_exist_raised/',
diff -Nru python-django-3.2.19/debian/patches/0027-CVE-2025-48432-2.patch python-django-3.2.25/debian/patches/0027-CVE-2025-48432-2.patch
--- python-django-3.2.19/debian/patches/0027-CVE-2025-48432-2.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0027-CVE-2025-48432-2.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,127 @@
+From: Jake Howard
+Date: Wed, 4 Jun 2025 16:08:46 +0100
+Subject: [PATCH] [4.2.x] Refs CVE-2025-48432 -- Prevented log injection in
+ remaining response logging.
+
+Migrated remaining response-related logging to use the `log_response()`
+helper to avoid potential log injection, to ensure untrusted values like
+request paths are safely escaped.
+
+Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
+
+Backport of 957951755259b412d5113333b32bf85871d29814 from main.
+---
+ django/views/generic/base.py | 17 +++++++++--------
+ tests/generic_views/test_base.py | 28 ++++++++++++++++++++++++++--
+ 2 files changed, 35 insertions(+), 10 deletions(-)
+
+diff --git a/django/views/generic/base.py b/django/views/generic/base.py
+index ab800ebce84b..a176d686f856 100644
+--- a/django/views/generic/base.py
++++ b/django/views/generic/base.py
+@@ -9,6 +9,7 @@ from django.http import (
+ from django.template.response import TemplateResponse
+ from django.urls import reverse
+ from django.utils.decorators import classonlymethod
++from django.utils.log import log_response
+
+ logger = logging.getLogger('django.request')
+
+@@ -98,11 +99,13 @@ class View:
+ return handler(request, *args, **kwargs)
+
+ def http_method_not_allowed(self, request, *args, **kwargs):
+- logger.warning(
++ response = HttpResponseNotAllowed(self._allowed_methods())
++ log_response(
+ 'Method Not Allowed (%s): %s', request.method, request.path,
+- extra={'status_code': 405, 'request': request}
++ response=response,
++ request=request,
+ )
+- return HttpResponseNotAllowed(self._allowed_methods())
++ return response
+
+ def options(self, request, *args, **kwargs):
+ """Handle responding to requests for the OPTIONS HTTP verb."""
+@@ -193,11 +196,9 @@ class RedirectView(View):
+ else:
+ return HttpResponseRedirect(url)
+ else:
+- logger.warning(
+- 'Gone: %s', request.path,
+- extra={'status_code': 410, 'request': request}
+- )
+- return HttpResponseGone()
++ response = HttpResponseGone()
++ log_response("Gone: %s", request.path, response=response, request=request)
++ return response
+
+ def head(self, request, *args, **kwargs):
+ return self.get(request, *args, **kwargs)
+diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py
+index f4b8a487ff4f..19e5ce654cf8 100644
+--- a/tests/generic_views/test_base.py
++++ b/tests/generic_views/test_base.py
+@@ -1,5 +1,8 @@
++import logging
+ import time
+
++from logging_tests.tests import LoggingAssertionMixin
++
+ from django.core.exceptions import ImproperlyConfigured
+ from django.http import HttpResponse
+ from django.test import RequestFactory, SimpleTestCase, override_settings
+@@ -64,7 +67,7 @@ class InstanceView(View):
+ return self
+
+
+-class ViewTest(SimpleTestCase):
++class ViewTest(LoggingAssertionMixin, SimpleTestCase):
+ rf = RequestFactory()
+
+ def _assert_simple(self, response):
+@@ -286,6 +289,17 @@ class ViewTest(SimpleTestCase):
+ response = view.dispatch(self.rf.head('/'))
+ self.assertEqual(response.status_code, 405)
+
++ def test_method_not_allowed_response_logged(self):
++ for path, escaped in [
++ ("/foo/", "/foo/"),
++ (r"/%1B[1;31mNOW IN RED!!!1B[0m/", r"/\x1b[1;31mNOW IN RED!!!1B[0m/"),
++ ]:
++ with self.subTest(path=path):
++ request = self.rf.get(path, REQUEST_METHOD="BOGUS")
++ with self.assertLogs("django.request", "WARNING") as handler:
++ response = SimpleView.as_view()(request)
++ self.assertEqual(response.status_code, 405)
++
+
+ @override_settings(ROOT_URLCONF='generic_views.urls')
+ class TemplateViewTest(SimpleTestCase):
+@@ -407,7 +421,7 @@ class TemplateViewTest(SimpleTestCase):
+
+
+ @override_settings(ROOT_URLCONF='generic_views.urls')
+-class RedirectViewTest(SimpleTestCase):
++class RedirectViewTest(LoggingAssertionMixin, SimpleTestCase):
+
+ rf = RequestFactory()
+
+@@ -519,6 +533,16 @@ class RedirectViewTest(SimpleTestCase):
+ response = view.dispatch(self.rf.head('/foo/'))
+ self.assertEqual(response.status_code, 410)
+
++ def test_gone_response_logged(self):
++ for path, escaped in [
++ ("/foo/", "/foo/"),
++ (r"/%1B[1;31mNOW IN RED!!!1B[0m/", r"/\x1b[1;31mNOW IN RED!!!1B[0m/"),
++ ]:
++ with self.subTest(path=path):
++ request = self.rf.get(path)
++ with self.assertLogs("django.request", "WARNING") as handler:
++ RedirectView().dispatch(request)
++
+
+ class GetContextDataTest(SimpleTestCase):
+
diff -Nru python-django-3.2.19/debian/patches/0028-CVE-2025-57833.patch python-django-3.2.25/debian/patches/0028-CVE-2025-57833.patch
--- python-django-3.2.19/debian/patches/0028-CVE-2025-57833.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0028-CVE-2025-57833.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,66 @@
+From: Jake Howard
+Date: Wed, 13 Aug 2025 14:13:42 +0200
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-57833 -- Protected FilteredRelation
+ against SQL injection in column aliases.
+
+Thanks Eyal Gabay (EyalSec) for the report.
+
+Backport of 51711717098d3f469f795dfa6bc3758b24f69ef7 from main.
+---
+ django/db/models/sql/query.py | 1 +
+ tests/annotations/tests.py | 23 +++++++++++++++++++++++
+ 2 files changed, 24 insertions(+)
+
+diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
+index 56afcda64f8b..0b3c313f27ba 100644
+--- a/django/db/models/sql/query.py
++++ b/django/db/models/sql/query.py
+@@ -1473,6 +1473,7 @@ class Query(BaseExpression):
+ return target_clause
+
+ def add_filtered_relation(self, filtered_relation, alias):
++ self.check_alias(alias)
+ filtered_relation.alias = alias
+ lookups = dict(get_children_from_q(filtered_relation.condition))
+ relation_lookup_parts, relation_field_parts, _ = self.solve_lookup_type(filtered_relation.relation_name)
+diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
+index 70dd793a6c7c..c554cdf90ac9 100644
+--- a/tests/annotations/tests.py
++++ b/tests/annotations/tests.py
+@@ -777,6 +777,24 @@ class NonAggregateAnnotationTestCase(TestCase):
+ with self.assertRaisesMessage(ValueError, msg):
+ Book.objects.annotate(**{crafted_alias: Value(1)})
+
++ def test_alias_filtered_relation_sql_injection(self):
++ crafted_alias = """injected_name" from "annotations_book"; --"""
++ msg = (
++ "Column aliases cannot contain whitespace characters, quotation marks, "
++ "semicolons, or SQL comments."
++ )
++ with self.assertRaisesMessage(ValueError, msg):
++ Book.objects.alias(**{crafted_alias: FilteredRelation("authors")})
++
++ def test_alias_filtered_relation_sql_injection(self):
++ crafted_alias = """injected_name" from "annotations_book"; --"""
++ msg = (
++ "Column aliases cannot contain whitespace characters, quotation marks, "
++ "semicolons, or SQL comments."
++ )
++ with self.assertRaisesMessage(ValueError, msg):
++ Book.objects.annotate(**{crafted_alias: FilteredRelation("author")})
++
+ def test_alias_forbidden_chars(self):
+ tests = [
+ 'al"ias',
+@@ -802,6 +820,11 @@ class NonAggregateAnnotationTestCase(TestCase):
+ with self.assertRaisesMessage(ValueError, msg):
+ Book.objects.annotate(**{crafted_alias: Value(1)})
+
++ with self.assertRaisesMessage(ValueError, msg):
++ Book.objects.annotate(
++ **{crafted_alias: FilteredRelation("authors")}
++ )
++
+
+ class AliasTests(TestCase):
+ @classmethod
diff -Nru python-django-3.2.19/debian/patches/0029-CVE-2025-59681.patch python-django-3.2.25/debian/patches/0029-CVE-2025-59681.patch
--- python-django-3.2.19/debian/patches/0029-CVE-2025-59681.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0029-CVE-2025-59681.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,166 @@
+From: Mariusz Felisiak
+Date: Wed, 10 Sep 2025 09:53:52 +0200
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-59681 -- Protected
+ QuerySet.annotate(), alias(), aggregate(),
+ and extra() against SQL injection in column aliases on MySQL/MariaDB.
+
+Thanks sw0rd1ight for the report.
+
+Follow up to 93cae5cb2f9a4ef1514cf1a41f714fef08005200.
+
+Backport of 41b43c74bda19753c757036673ea9db74acf494a from main.
+---
+ django/db/models/sql/query.py | 8 ++++----
+ tests/aggregation/tests.py | 4 ++--
+ tests/annotations/tests.py | 21 +++++++++++----------
+ tests/expressions/test_queryset_values.py | 8 ++++----
+ tests/queries/tests.py | 4 ++--
+ 5 files changed, 23 insertions(+), 22 deletions(-)
+
+diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
+index 0b3c313f27ba..85dab7b47631 100644
+--- a/django/db/models/sql/query.py
++++ b/django/db/models/sql/query.py
+@@ -46,9 +46,9 @@ from django.utils.tree import Node
+
+ __all__ = ['Query', 'RawQuery']
+
+-# Quotation marks ('"`[]), whitespace characters, semicolons, or inline
++# Quotation marks ('"`[]), whitespace characters, semicolons, hashes, or inline
+ # SQL comments are forbidden in column aliases.
+-FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|--|/\*|\*/")
++FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|#|--|/\*|\*/")
+
+ # Inspired from
+ # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
+@@ -1051,8 +1051,8 @@ class Query(BaseExpression):
+ def check_alias(self, alias):
+ if FORBIDDEN_ALIAS_PATTERN.search(alias):
+ raise ValueError(
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, "
++ "quotation marks, semicolons, or SQL comments."
+ )
+
+ def add_annotation(self, annotation, alias, is_summary=False, select=True):
+diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
+index 48502c4a4a25..827247721068 100644
+--- a/tests/aggregation/tests.py
++++ b/tests/aggregation/tests.py
+@@ -1371,8 +1371,8 @@ class AggregateTestCase(TestCase):
+ def test_alias_sql_injection(self):
+ crafted_alias = """injected_name" from "aggregation_author"; --"""
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ Author.objects.aggregate(**{crafted_alias: Avg("age")})
+diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
+index c554cdf90ac9..1d42e046def1 100644
+--- a/tests/annotations/tests.py
++++ b/tests/annotations/tests.py
+@@ -771,8 +771,8 @@ class NonAggregateAnnotationTestCase(TestCase):
+ def test_alias_sql_injection(self):
+ crafted_alias = """injected_name" from "annotations_book"; --"""
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ Book.objects.annotate(**{crafted_alias: Value(1)})
+@@ -780,7 +780,7 @@ class NonAggregateAnnotationTestCase(TestCase):
+ def test_alias_filtered_relation_sql_injection(self):
+ crafted_alias = """injected_name" from "annotations_book"; --"""
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
++ "Column aliases cannot contain whitespace characters, hashes, quotation marks, "
+ "semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+@@ -789,8 +789,8 @@ class NonAggregateAnnotationTestCase(TestCase):
+ def test_alias_filtered_relation_sql_injection(self):
+ crafted_alias = """injected_name" from "annotations_book"; --"""
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ Book.objects.annotate(**{crafted_alias: FilteredRelation("author")})
+@@ -807,13 +807,14 @@ class NonAggregateAnnotationTestCase(TestCase):
+ "ali/*as",
+ "alias*/",
+ "alias;",
+- # [] are used by MSSQL.
++ # [] and # are used by MSSQL.
+ "alias[",
+ "alias]",
++ "ali#as",
+ ]
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ for crafted_alias in tests:
+ with self.subTest(crafted_alias):
+@@ -1059,8 +1060,8 @@ class AliasTests(TestCase):
+ def test_alias_sql_injection(self):
+ crafted_alias = """injected_name" from "annotations_book"; --"""
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ Book.objects.alias(**{crafted_alias: Value(1)})
+diff --git a/tests/expressions/test_queryset_values.py b/tests/expressions/test_queryset_values.py
+index 2b524d8d99fe..97bfa107e07b 100644
+--- a/tests/expressions/test_queryset_values.py
++++ b/tests/expressions/test_queryset_values.py
+@@ -29,8 +29,8 @@ class ValuesExpressionsTests(TestCase):
+ def test_values_expression_alias_sql_injection(self):
+ crafted_alias = """injected_name" from "expressions_company"; --"""
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ Company.objects.values(**{crafted_alias: F("ceo__salary")})
+@@ -39,8 +39,8 @@ class ValuesExpressionsTests(TestCase):
+ 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."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ JSONFieldModel.objects.values(f"data__{crafted_alias}")
+diff --git a/tests/queries/tests.py b/tests/queries/tests.py
+index 4e6ff67e4906..e6ab6dffe814 100644
+--- a/tests/queries/tests.py
++++ b/tests/queries/tests.py
+@@ -1680,8 +1680,8 @@ class Queries5Tests(TestCase):
+ def test_extra_select_alias_sql_injection(self):
+ crafted_alias = """injected_name" from "queries_note"; --"""
+ msg = (
+- "Column aliases cannot contain whitespace characters, quotation marks, "
+- "semicolons, or SQL comments."
++ "Column aliases cannot contain whitespace characters, hashes, quotation "
++ "marks, semicolons, or SQL comments."
+ )
+ with self.assertRaisesMessage(ValueError, msg):
+ Note.objects.extra(select={crafted_alias: "1"})
diff -Nru python-django-3.2.19/debian/patches/0030-CVE-2025-59682.patch python-django-3.2.25/debian/patches/0030-CVE-2025-59682.patch
--- python-django-3.2.19/debian/patches/0030-CVE-2025-59682.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0030-CVE-2025-59682.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,66 @@
+From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
+Date: Tue, 16 Sep 2025 17:13:36 +0200
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-59682 -- Fixed potential partial
+ directory-traversal via archive.extract().
+
+Thanks stackered for the report.
+
+Follow up to 05413afa8c18cdb978fcdf470e09f7a12b234a23.
+
+Backport of 924a0c092e65fa2d0953fd1855d2dc8786d94de2 from main.
+---
+ django/utils/archive.py | 6 +++++-
+ tests/utils_tests/test_archive.py | 19 +++++++++++++++++++
+ 2 files changed, 24 insertions(+), 1 deletion(-)
+
+diff --git a/django/utils/archive.py b/django/utils/archive.py
+index d5a0cf044657..0c9e4feb0d74 100644
+--- a/django/utils/archive.py
++++ b/django/utils/archive.py
+@@ -138,7 +138,11 @@ class BaseArchive:
+ def target_filename(self, to_path, name):
+ target_path = os.path.abspath(to_path)
+ filename = os.path.abspath(os.path.join(target_path, name))
+- if not filename.startswith(target_path):
++ try:
++ if os.path.commonpath([target_path, filename]) != target_path:
++ raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
++ except ValueError:
++ # Different drives on Windows raises ValueError.
+ raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
+ return filename
+
+diff --git a/tests/utils_tests/test_archive.py b/tests/utils_tests/test_archive.py
+index 4eb47d2fad76..74981187f36d 100644
+--- a/tests/utils_tests/test_archive.py
++++ b/tests/utils_tests/test_archive.py
+@@ -3,6 +3,7 @@ import stat
+ import sys
+ import tempfile
+ import unittest
++import zipfile
+
+ from django.core.exceptions import SuspiciousOperation
+ from django.test import SimpleTestCase
+@@ -87,3 +88,21 @@ class TestArchiveInvalid(SimpleTestCase):
+ with self.subTest(entry), tempfile.TemporaryDirectory() as tmpdir:
+ with self.assertRaisesMessage(SuspiciousOperation, msg % invalid_path):
+ archive.extract(os.path.join(archives_dir, entry), tmpdir)
++
++ def test_extract_function_traversal_startswith(self):
++ with tempfile.TemporaryDirectory() as tmpdir:
++ base = os.path.abspath(tmpdir)
++ tarfile_handle = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
++ tar_path = tarfile_handle.name
++ tarfile_handle.close()
++ self.addCleanup(os.remove, tar_path)
++
++ malicious_member = os.path.join(base + "abc", "evil.txt")
++ with zipfile.ZipFile(tar_path, "w") as zf:
++ zf.writestr(malicious_member, "evil\n")
++ zf.writestr("test.txt", "data\n")
++
++ with self.assertRaisesMessage(
++ SuspiciousOperation, "Archive contains invalid path"
++ ):
++ archive.extract(tar_path, base)
diff -Nru python-django-3.2.19/debian/patches/0031-CVE-2025-64459.patch python-django-3.2.25/debian/patches/0031-CVE-2025-64459.patch
--- python-django-3.2.19/debian/patches/0031-CVE-2025-64459.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0031-CVE-2025-64459.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,44 @@
+From: Jacob Walls
+Date: Wed, 24 Sep 2025 15:54:51 -0400
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-64459 -- Prevented SQL injections in
+ Q/QuerySet via the _connector kwarg.
+
+Thanks cyberstan for the report, Sarah Boyce, Adam Johnson, Simon
+Charette, and Jake Howard for the reviews.
+
+Backport of c880530ddd4fabd5939bab0e148bebe36699432a from main.
+---
+ django/db/models/query_utils.py | 4 ++++
+ tests/queries/test_q.py | 5 +++++
+ 2 files changed, 9 insertions(+)
+
+diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
+index e8ad7cd63553..ae364bca24ad 100644
+--- a/django/db/models/query_utils.py
++++ b/django/db/models/query_utils.py
+@@ -64,8 +64,12 @@ class Q(tree.Node):
+ OR = 'OR'
+ default = AND
+ conditional = True
++ connectors = (None, AND, OR)
+
+ def __init__(self, *args, _connector=None, _negated=False, **kwargs):
++ if _connector not in self.connectors:
++ connector_reprs = ", ".join(f"{conn!r}" for conn in self.connectors[1:])
++ raise ValueError(f"_connector must be one of {connector_reprs}, or None.")
+ super().__init__(children=[*args, *sorted(kwargs.items())], connector=_connector, negated=_negated)
+
+ def _combine(self, other, conn):
+diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py
+index d75751be6382..885699012696 100644
+--- a/tests/queries/test_q.py
++++ b/tests/queries/test_q.py
+@@ -128,3 +128,8 @@ class QTests(SimpleTestCase):
+ q = q1 & q2
+ path, args, kwargs = q.deconstruct()
+ self.assertEqual(Q(*args, **kwargs), q)
++
++ def test_connector_validation(self):
++ msg = f"_connector must be one of {Q.AND!r}, {Q.OR!r}, or None."
++ with self.assertRaisesMessage(ValueError, msg):
++ Q(_connector="evil")
diff -Nru python-django-3.2.19/debian/patches/0032-CVE-2025-64460.patch python-django-3.2.25/debian/patches/0032-CVE-2025-64460.patch
--- python-django-3.2.19/debian/patches/0032-CVE-2025-64460.patch 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/debian/patches/0032-CVE-2025-64460.patch 2026-01-27 19:16:59.000000000 +0000
@@ -0,0 +1,102 @@
+From: Shai Berger
+Date: Sat, 11 Oct 2025 21:42:56 +0300
+Subject: [PATCH] [4.2.x] Fixed CVE-2025-64460 -- Corrected quadratic inner
+ text accumulation in XML serializer.
+
+Previously, `getInnerText()` recursively used `list.extend()` on strings,
+which added each character from child nodes as a separate list element.
+On deeply nested XML content, this caused the overall deserialization
+work to grow quadratically with input size, potentially allowing
+disproportionate CPU consumption for crafted XML.
+
+The fix separates collection of inner texts from joining them, so that
+each subtree is joined only once, reducing the complexity to linear in
+the size of the input. These changes also include a mitigation for a
+xml.dom.minidom performance issue.
+
+Thanks Seokchan Yoon (https://ch4n3.kr/) for report.
+
+Co-authored-by: Jacob Walls
+Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
+
+Backport of 50efb718b31333051bc2dcb06911b8fa1358c98c from main.
+---
+ django/core/serializers/xml_serializer.py | 38 ++++++++++++++++++++++++++-----
+ 1 file changed, 32 insertions(+), 6 deletions(-)
+
+diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
+index 88bfa590321e..8d266cae769b 100644
+--- a/django/core/serializers/xml_serializer.py
++++ b/django/core/serializers/xml_serializer.py
+@@ -2,7 +2,8 @@
+ XML serializer.
+ """
+ import json
+-from xml.dom import pulldom
++from contextlib import contextmanager
++from xml.dom import minidom, pulldom
+ from xml.sax import handler
+ from xml.sax.expatreader import ExpatParser as _ExpatParser
+
+@@ -16,6 +17,25 @@ from django.utils.xmlutils import (
+ )
+
+
++@contextmanager
++def fast_cache_clearing():
++ """Workaround for performance issues in minidom document checks.
++
++ Speeds up repeated DOM operations by skipping unnecessary full traversal
++ of the DOM tree.
++ """
++ module_helper_was_lambda = False
++ if original_fn := getattr(minidom, "_in_document", None):
++ module_helper_was_lambda = original_fn.__name__ == ""
++ if not module_helper_was_lambda:
++ minidom._in_document = lambda node: bool(node.ownerDocument)
++ try:
++ yield
++ finally:
++ if original_fn and not module_helper_was_lambda:
++ minidom._in_document = original_fn
++
++
+ class Serializer(base.Serializer):
+ """Serialize a QuerySet to XML."""
+
+@@ -173,7 +193,8 @@ class Deserializer(base.Deserializer):
+ def __next__(self):
+ for event, node in self.event_stream:
+ if event == "START_ELEMENT" and node.nodeName == "object":
+- self.event_stream.expandNode(node)
++ with fast_cache_clearing():
++ self.event_stream.expandNode(node)
+ return self._handle_object(node)
+ raise StopIteration
+
+@@ -334,16 +355,21 @@ class Deserializer(base.Deserializer):
+
+ def getInnerText(node):
+ """Get all the inner text of a DOM node (recursively)."""
++ inner_text_list = getInnerTextList(node)
++ return "".join(inner_text_list)
++
++def getInnerTextList(node):
++ """Return a list of the inner texts of a DOM node (recursively)."""
+ # inspired by https://mail.python.org/pipermail/xml-sig/2005-March/011022.html
+- inner_text = []
++ result = []
+ for child in node.childNodes:
+ if child.nodeType == child.TEXT_NODE or child.nodeType == child.CDATA_SECTION_NODE:
+- inner_text.append(child.data)
++ result.append(child.data)
+ elif child.nodeType == child.ELEMENT_NODE:
+- inner_text.extend(getInnerText(child))
++ result.extend(getInnerTextList(child))
+ else:
+ pass
+- return "".join(inner_text)
++ return result
+
+
+ # Below code based on Christian Heimes' defusedxml
diff -Nru python-django-3.2.19/debian/patches/series python-django-3.2.25/debian/patches/series
--- python-django-3.2.19/debian/patches/series 2024-08-21 11:08:24.000000000 +0000
+++ python-django-3.2.25/debian/patches/series 2026-01-27 19:16:59.000000000 +0000
@@ -10,11 +10,22 @@
0011-Moved-RequestSite-import-to-the-toplevel.patch
0012-Add-Python-3.11-support-for-tests.patch
0013-fix-url-validator.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
+0014-CVE-2024-39329.patch
+0015-CVE-2024-39330.patch
+0016-CVE-2024-39614.patch
+0017-CVE-2024-41989.patch
+0018-CVE-2024-41991.patch
+0019-CVE-2024-42005.patch
+0020-CVE-2024-45231.patch
+0021-CVE-2024-53907.patch
+0022-CVE-2024-56374.patch
+0023-CVE-2025-13372.patch
+0024-CVE-2025-26699.patch
+0025-CVE-2025-32873.patch
+0026-CVE-2025-48432.patch
+0027-CVE-2025-48432-2.patch
+0028-CVE-2025-57833.patch
+0029-CVE-2025-59681.patch
+0030-CVE-2025-59682.patch
+0031-CVE-2025-64459.patch
+0032-CVE-2025-64460.patch
diff -Nru python-django-3.2.19/django/__init__.py python-django-3.2.25/django/__init__.py
--- python-django-3.2.19/django/__init__.py 2023-05-03 11:59:16.000000000 +0000
+++ python-django-3.2.25/django/__init__.py 2024-03-04 07:48:09.000000000 +0000
@@ -1,6 +1,6 @@
from django.utils.version import get_version
-VERSION = (3, 2, 19, 'final', 0)
+VERSION = (3, 2, 25, 'final', 0)
__version__ = get_version(VERSION)
diff -Nru python-django-3.2.19/django/contrib/auth/forms.py python-django-3.2.25/django/contrib/auth/forms.py
--- python-django-3.2.19/django/contrib/auth/forms.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/django/contrib/auth/forms.py 2024-03-04 07:47:52.000000000 +0000
@@ -62,7 +62,15 @@
class UsernameField(forms.CharField):
def to_python(self, value):
- return unicodedata.normalize('NFKC', super().to_python(value))
+ value = super().to_python(value)
+ if self.max_length is not None and len(value) > self.max_length:
+ # Normalization can increase the string length (e.g.
+ # "ff" -> "ff", "½" -> "1⁄2") but cannot reduce it, so there is no
+ # point in normalizing invalid data. Moreover, Unicode
+ # normalization is very slow on Windows and can be a DoS attack
+ # vector.
+ return value
+ return unicodedata.normalize("NFKC", value)
def widget_attrs(self, widget):
return {
diff -Nru python-django-3.2.19/django/contrib/humanize/templatetags/humanize.py python-django-3.2.25/django/contrib/humanize/templatetags/humanize.py
--- python-django-3.2.19/django/contrib/humanize/templatetags/humanize.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/django/contrib/humanize/templatetags/humanize.py 2024-03-04 07:47:52.000000000 +0000
@@ -70,12 +70,15 @@
return intcomma(value, False)
else:
return number_format(value, use_l10n=True, force_grouping=True)
- orig = str(value)
- new = re.sub(r"^(-?\d+)(\d{3})", r'\g<1>,\g<2>', orig)
- if orig == new:
- return new
- else:
- return intcomma(new, use_l10n)
+ result = str(value)
+ match = re.match(r"-?\d+", result)
+ if match:
+ prefix = match[0]
+ prefix_with_commas = re.sub(r"\d{3}", r"\g<0>,", prefix[::-1])[::-1]
+ # Remove a leading comma, if needed.
+ prefix_with_commas = re.sub(r"^(-?),", r"\1", prefix_with_commas)
+ result = prefix_with_commas + result[len(prefix) :]
+ return result
# A tuple of standard large number to their converters
diff -Nru python-django-3.2.19/django/core/validators.py python-django-3.2.25/django/core/validators.py
--- python-django-3.2.19/django/core/validators.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/django/core/validators.py 2024-03-04 07:47:52.000000000 +0000
@@ -93,6 +93,7 @@
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 @@
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})
@@ -210,7 +211,9 @@
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 -Nru python-django-3.2.19/django/forms/fields.py python-django-3.2.25/django/forms/fields.py
--- python-django-3.2.19/django/forms/fields.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/django/forms/fields.py 2024-03-04 07:47:52.000000000 +0000
@@ -540,6 +540,9 @@
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 -Nru python-django-3.2.19/django/utils/encoding.py python-django-3.2.25/django/utils/encoding.py
--- python-django-3.2.19/django/utils/encoding.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/django/utils/encoding.py 2024-03-04 07:47:52.000000000 +0000
@@ -229,6 +229,7 @@
repercent-encode any octet produced that is not part of a strictly legal
UTF-8 octet sequence.
"""
+ changed_parts = []
while True:
try:
path.decode()
@@ -236,9 +237,10 @@
# CVE-2019-14235: A recursion shouldn't be used since the exception
# handling uses massive amounts of memory
repercent = quote(path[e.start:e.end], safe=b"/#%[]=:;$&()+,!?*@'~")
- path = path[:e.start] + repercent.encode() + path[e.end:]
+ changed_parts.append(path[:e.start] + repercent.encode())
+ path = path[e.end:]
else:
- return path
+ return b"".join(changed_parts) + path
def filepath_to_uri(path):
diff -Nru python-django-3.2.19/django/utils/text.py python-django-3.2.25/django/utils/text.py
--- python-django-3.2.19/django/utils/text.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/django/utils/text.py 2024-03-04 07:47:52.000000000 +0000
@@ -18,8 +18,61 @@
return x and str(x)[0].upper() + str(x)[1:]
-# Set up regular expressions
-re_words = _lazy_re_compile(r'<[^>]+?>|([^<>\s]+)', re.S)
+# ----- Begin security-related performance workaround -----
+
+# We used to have, below
+#
+# re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S)
+#
+# But it was shown that this regex, in the way we use it here, has some
+# catastrophic edge-case performance features. Namely, when it is applied to
+# text with only open brackets "<<<...". The class below provides the services
+# and correct answers for the use cases, but in these edge cases does it much
+# faster.
+re_notag = _lazy_re_compile(r"([^<>\s]+)", re.S)
+re_prt = _lazy_re_compile(r"<|([^<>\s]+)", re.S)
+
+
+class WordsRegex:
+ @staticmethod
+ def search(text, pos):
+ # Look for "<" or a non-tag word.
+ partial = re_prt.search(text, pos)
+ if partial is None or partial[1] is not None:
+ return partial
+
+ # "<" was found, look for a closing ">".
+ end = text.find(">", partial.end(0))
+ if end < 0:
+ # ">" cannot be found, look for a word.
+ return re_notag.search(text, pos + 1)
+ else:
+ # "<" followed by a ">" was found -- fake a match.
+ end += 1
+ return FakeMatch(text[partial.start(0): end], end)
+
+
+class FakeMatch:
+ __slots__ = ["_text", "_end"]
+
+ def end(self, group=0):
+ assert group == 0, "This specific object takes only group=0"
+ return self._end
+
+ def __getitem__(self, group):
+ if group == 1:
+ return None
+ assert group == 0, "This specific object takes only group in {0,1}"
+ return self._text
+
+ def __init__(self, text, end):
+ self._text, self._end = text, end
+
+
+# ----- End security-related performance workaround -----
+
+# Set up regular expressions.
+re_words = WordsRegex
re_chars = _lazy_re_compile(r'<[^>]+?>|(.)', re.S)
re_tag = _lazy_re_compile(r'<(/)?(\S+?)(?:(\s*/)|\s.*?)?>', re.S)
re_newlines = _lazy_re_compile(r'\r\n|\r') # Used in normalize_newlines
@@ -60,7 +113,14 @@
class Truncator(SimpleLazyObject):
"""
An object used to truncate text, either by characters or words.
+
+ When truncating HTML text (either chars or words), input will be limited to
+ at most `MAX_LENGTH_HTML` characters.
"""
+
+ # 5 million characters are approximately 4000 text pages or 3 web pages.
+ MAX_LENGTH_HTML = 5_000_000
+
def __init__(self, text):
super().__init__(lambda: str(text))
@@ -157,6 +217,11 @@
if words and length <= 0:
return ''
+ size_limited = False
+ if len(text) > self.MAX_LENGTH_HTML:
+ text = text[: self.MAX_LENGTH_HTML]
+ size_limited = True
+
html4_singlets = (
'br', 'col', 'link', 'base', 'img',
'param', 'area', 'hr', 'input'
@@ -206,10 +271,14 @@
# Add it to the start of the open tags list
open_tags.insert(0, tagname)
+ truncate_text = self.add_truncation_text("", truncate)
+
if current_len <= length:
+ if size_limited and truncate_text:
+ text += truncate_text
return text
+
out = text[:end_text_pos]
- truncate_text = self.add_truncation_text('', truncate)
if truncate_text:
out += truncate_text
# Close any tags still open
diff -Nru python-django-3.2.19/docs/_ext/djangodocs.py python-django-3.2.25/docs/_ext/djangodocs.py
--- python-django-3.2.19/docs/_ext/djangodocs.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/docs/_ext/djangodocs.py 2024-03-04 07:47:52.000000000 +0000
@@ -130,10 +130,23 @@
def visit_desc_parameterlist(self, node):
self.body.append('(') # by default sphinx puts around the "("
- self.first_param = 1
self.optional_param_level = 0
self.param_separator = node.child_text_separator
- self.required_params_left = sum(isinstance(c, addnodes.desc_parameter) for c in node.children)
+ # Counts 'parameter groups' being either a required parameter, or a set
+ # of contiguous optional ones.
+ required_params = [
+ isinstance(c, addnodes.desc_parameter) for c in node.children
+ ]
+ # How many required parameters are left.
+ self.required_params_left = sum(required_params)
+ if sphinx_version < (7, 1):
+ self.first_param = 1
+ else:
+ self.is_first_param = True
+ self.params_left_at_level = 0
+ self.param_group_index = 0
+ self.list_is_required_param = required_params
+ self.multi_line_parameter_list = False
def depart_desc_parameterlist(self, node):
self.body.append(')')
diff -Nru python-django-3.2.19/docs/ref/forms/fields.txt python-django-3.2.25/docs/ref/forms/fields.txt
--- python-django-3.2.19/docs/ref/forms/fields.txt 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/docs/ref/forms/fields.txt 2024-03-04 07:47:52.000000000 +0000
@@ -592,7 +592,12 @@
* 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 -Nru python-django-3.2.19/docs/ref/templates/builtins.txt python-django-3.2.25/docs/ref/templates/builtins.txt
--- python-django-3.2.19/docs/ref/templates/builtins.txt 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/docs/ref/templates/builtins.txt 2024-03-04 07:47:52.000000000 +0000
@@ -2348,6 +2348,16 @@
Newlines in the HTML content will be preserved.
+.. admonition:: Size of input string
+
+ Processing large, potentially malformed HTML strings can be
+ resource-intensive and impact service performance. ``truncatechars_html``
+ limits input to the first five million characters.
+
+.. versionchanged:: 3.2.22
+
+ In older versions, strings over five million characters were processed.
+
.. templatefilter:: truncatewords
``truncatewords``
@@ -2386,6 +2396,16 @@
Newlines in the HTML content will be preserved.
+.. admonition:: Size of input string
+
+ Processing large, potentially malformed HTML strings can be
+ resource-intensive and impact service performance. ``truncatewords_html``
+ limits input to the first five million characters.
+
+.. versionchanged:: 3.2.22
+
+ In older versions, strings over five million characters were processed.
+
.. templatefilter:: unordered_list
``unordered_list``
diff -Nru python-django-3.2.19/docs/ref/utils.txt python-django-3.2.25/docs/ref/utils.txt
--- python-django-3.2.19/docs/ref/utils.txt 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/docs/ref/utils.txt 2024-03-04 07:47:52.000000000 +0000
@@ -276,7 +276,7 @@
.. deprecated:: 3.0
- Alias of :func:`force_str` for backwards compatibility, especially in code
+ Alias of :func:`smart_str` for backwards compatibility, especially in code
that supports Python 2.
.. function:: force_text(s, encoding='utf-8', strings_only=False, errors='strict')
diff -Nru python-django-3.2.19/docs/ref/validators.txt python-django-3.2.25/docs/ref/validators.txt
--- python-django-3.2.19/docs/ref/validators.txt 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/docs/ref/validators.txt 2024-03-04 07:47:52.000000000 +0000
@@ -130,6 +130,11 @@
: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 @@
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 @@
.. _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 -Nru python-django-3.2.19/docs/releases/3.2.20.txt python-django-3.2.25/docs/releases/3.2.20.txt
--- python-django-3.2.19/docs/releases/3.2.20.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/docs/releases/3.2.20.txt 2023-07-03 06:32:22.000000000 +0000
@@ -0,0 +1,14 @@
+===========================
+Django 3.2.20 release notes
+===========================
+
+*July 3, 2023*
+
+Django 3.2.20 fixes a security issue with severity "moderate" in 3.2.19.
+
+CVE-2023-36053: Potential regular expression denial of service vulnerability in ``EmailValidator``/``URLValidator``
+===================================================================================================================
+
+``EmailValidator`` and ``URLValidator`` were subject to potential regular
+expression denial of service attack via a very large number of domain name
+labels of emails and URLs.
diff -Nru python-django-3.2.19/docs/releases/3.2.21.txt python-django-3.2.25/docs/releases/3.2.21.txt
--- python-django-3.2.19/docs/releases/3.2.21.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/docs/releases/3.2.21.txt 2023-09-04 10:23:08.000000000 +0000
@@ -0,0 +1,14 @@
+===========================
+Django 3.2.21 release notes
+===========================
+
+*September 4, 2023*
+
+Django 3.2.21 fixes a security issue with severity "moderate" in 3.2.20.
+
+CVE-2023-41164: Potential denial of service vulnerability in ``django.utils.encoding.uri_to_iri()``
+===================================================================================================
+
+``django.utils.encoding.uri_to_iri()`` was subject to potential denial of
+service attack via certain inputs with a very large number of Unicode
+characters.
diff -Nru python-django-3.2.19/docs/releases/3.2.22.txt python-django-3.2.25/docs/releases/3.2.22.txt
--- python-django-3.2.19/docs/releases/3.2.22.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/docs/releases/3.2.22.txt 2023-11-01 05:09:28.000000000 +0000
@@ -0,0 +1,25 @@
+===========================
+Django 3.2.22 release notes
+===========================
+
+*October 4, 2023*
+
+Django 3.2.22 fixes a security issue with severity "moderate" in 3.2.21.
+
+CVE-2023-43665: Denial-of-service possibility in ``django.utils.text.Truncator``
+================================================================================
+
+Following the fix for :cve:`2019-14232`, the regular expressions used in the
+implementation of ``django.utils.text.Truncator``'s ``chars()`` and ``words()``
+methods (with ``html=True``) were revised and improved. However, these regular
+expressions still exhibited linear backtracking complexity, so when given a
+very long, potentially malformed HTML input, the evaluation would still be
+slow, leading to a potential denial of service vulnerability.
+
+The ``chars()`` and ``words()`` methods are used to implement the
+:tfilter:`truncatechars_html` and :tfilter:`truncatewords_html` template
+filters, which were thus also vulnerable.
+
+The input processed by ``Truncator``, when operating in HTML mode, has been
+limited to the first five million characters in order to avoid potential
+performance and memory issues.
diff -Nru python-django-3.2.19/docs/releases/3.2.23.txt python-django-3.2.25/docs/releases/3.2.23.txt
--- python-django-3.2.19/docs/releases/3.2.23.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/docs/releases/3.2.23.txt 2023-11-01 05:30:53.000000000 +0000
@@ -0,0 +1,19 @@
+===========================
+Django 3.2.23 release notes
+===========================
+
+*November 1, 2023*
+
+Django 3.2.23 fixes a security issue with severity "moderate" in 3.2.22.
+
+CVE-2023-46695: Potential denial of service vulnerability in ``UsernameField`` on Windows
+=========================================================================================
+
+The :func:`NFKC normalization ` is slow on
+Windows. As a consequence, ``django.contrib.auth.forms.UsernameField`` was
+subject to a potential denial of service attack via certain inputs with a very
+large number of Unicode characters.
+
+In order to avoid the vulnerability, invalid values longer than
+``UsernameField.max_length`` are no longer normalized, since they cannot pass
+validation anyway.
diff -Nru python-django-3.2.19/docs/releases/3.2.24.txt python-django-3.2.25/docs/releases/3.2.24.txt
--- python-django-3.2.19/docs/releases/3.2.24.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/docs/releases/3.2.24.txt 2024-03-04 07:21:02.000000000 +0000
@@ -0,0 +1,13 @@
+===========================
+Django 3.2.24 release notes
+===========================
+
+*February 6, 2024*
+
+Django 3.2.24 fixes a security issue with severity "moderate" in 3.2.23.
+
+CVE-2024-24680: Potential denial-of-service in ``intcomma`` template filter
+===========================================================================
+
+The ``intcomma`` template filter was subject to a potential denial-of-service
+attack when used with very long strings.
diff -Nru python-django-3.2.19/docs/releases/3.2.25.txt python-django-3.2.25/docs/releases/3.2.25.txt
--- python-django-3.2.19/docs/releases/3.2.25.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-3.2.25/docs/releases/3.2.25.txt 2024-03-04 07:37:35.000000000 +0000
@@ -0,0 +1,22 @@
+===========================
+Django 3.2.25 release notes
+===========================
+
+*March 4, 2024*
+
+Django 3.2.25 fixes a security issue with severity "moderate" and a regression
+in 3.2.24.
+
+CVE-2024-27351: Potential regular expression denial-of-service in ``django.utils.text.Truncator.words()``
+=========================================================================================================
+
+``django.utils.text.Truncator.words()`` method (with ``html=True``) and
+:tfilter:`truncatewords_html` template filter were subject to a potential
+regular expression denial-of-service attack using a suitably crafted string
+(follow up to :cve:`2019-14232` and :cve:`2023-43665`).
+
+Bugfixes
+========
+
+* Fixed a regression in Django 3.2.24 where ``intcomma`` template filter could
+ return a leading comma for string representation of floats (:ticket:`35172`).
diff -Nru python-django-3.2.19/docs/releases/index.txt python-django-3.2.25/docs/releases/index.txt
--- python-django-3.2.19/docs/releases/index.txt 2023-05-03 11:58:23.000000000 +0000
+++ python-django-3.2.25/docs/releases/index.txt 2024-03-04 07:47:52.000000000 +0000
@@ -25,6 +25,12 @@
.. toctree::
:maxdepth: 1
+ 3.2.25
+ 3.2.24
+ 3.2.23
+ 3.2.22
+ 3.2.21
+ 3.2.20
3.2.19
3.2.18
3.2.17
diff -Nru python-django-3.2.19/docs/releases/security.txt python-django-3.2.25/docs/releases/security.txt
--- python-django-3.2.19/docs/releases/security.txt 2023-05-03 11:58:23.000000000 +0000
+++ python-django-3.2.25/docs/releases/security.txt 2024-03-04 07:21:02.000000000 +0000
@@ -36,6 +36,72 @@
All security issues have been handled under versions of Django's security
process. These are listed below.
+February 6, 2024 - :cve:`2024-24680`
+------------------------------------
+
+Potential denial-of-service in ``intcomma`` template filter.
+`Full description
+ `__
+
+* Django 5.0 :commit:`(patch) <16a8fe18a3b81250f4fa57e3f93f0599dc4895bc>`
+* Django 4.2 :commit:`(patch) <572ea07e84b38ea8de0551f4b4eda685d91d09d2>`
+* Django 3.2 :commit:`(patch) `
+
+November 1, 2023 - :cve:`2023-46695`
+------------------------------------
+
+Potential denial of service vulnerability in ``UsernameField`` on Windows.
+`Full description
+ `__
+
+* Django 4.2 :commit:`(patch) <048a9ebb6ea468426cb4e57c71572cbbd975517f>`
+* Django 4.1 :commit:`(patch) <4965bfdde2e5a5c883685019e57d123a3368a75e>`
+* Django 3.2 :commit:`(patch) `
+
+October 4, 2023 - :cve:`2023-43665`
+-----------------------------------
+
+Denial-of-service possibility in ``django.utils.text.Truncator``.
+`Full description
+ `__
+
+* Django 4.2 :commit:`(patch) `
+* Django 4.1 :commit:`(patch) `
+* Django 3.2 :commit:`(patch) `
+
+September 4, 2023 - :cve:`2023-41164`
+-------------------------------------
+
+Potential denial of service vulnerability in
+``django.utils.encoding.uri_to_iri()``. `Full description
+ `__
+
+* Django 4.2 :commit:`(patch) <9c51b4dcfa0cefcb48231f4d71cafa80821f87b9>`
+* Django 4.1 :commit:`(patch) `
+* Django 3.2 :commit:`(patch) <6f030b1149bd8fa4ba90452e77cb3edc095ce54e>`
+
+July 3, 2023 - :cve:`2023-36053`
+--------------------------------
+
+Potential regular expression denial of service vulnerability in
+``EmailValidator``/``URLValidator``. `Full description
+ `__
+
+* Django 4.2 :commit:`(patch) `
+* Django 4.1 :commit:`(patch) `
+* Django 3.2 :commit:`(patch) <454f2fb93437f98917283336201b4048293f7582>`
+
+May 3, 2023 - :cve:`2023-31047`
+-------------------------------
+
+Potential bypass of validation when uploading multiple files using one form
+field. `Full description
+ `__
+
+* Django 4.2 :commit:`(patch) <21b1b1fc03e5f9e9f8c977ee6e35618dd3b353dd>`
+* Django 4.1 :commit:`(patch) `
+* Django 3.2 :commit:`(patch) `
+
February 14, 2023 - :cve:`2023-24580`
-------------------------------------
diff -Nru python-django-3.2.19/tests/auth_tests/test_forms.py python-django-3.2.25/tests/auth_tests/test_forms.py
--- python-django-3.2.19/tests/auth_tests/test_forms.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/tests/auth_tests/test_forms.py 2024-03-04 07:47:52.000000000 +0000
@@ -5,7 +5,7 @@
from django.contrib.auth.forms import (
AdminPasswordChangeForm, AuthenticationForm, PasswordChangeForm,
PasswordResetForm, ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget,
- SetPasswordForm, UserChangeForm, UserCreationForm,
+ SetPasswordForm, UserChangeForm, UserCreationForm, UsernameField,
)
from django.contrib.auth.models import User
from django.contrib.auth.signals import user_login_failed
@@ -132,6 +132,12 @@
self.assertNotEqual(user.username, ohm_username)
self.assertEqual(user.username, 'testΩ') # U+03A9 GREEK CAPITAL LETTER OMEGA
+ def test_invalid_username_no_normalize(self):
+ field = UsernameField(max_length=254)
+ # Usernames are not normalized if they are too long.
+ self.assertEqual(field.to_python("½" * 255), "½" * 255)
+ self.assertEqual(field.to_python("ff" * 254), "ff" * 254)
+
def test_duplicate_normalized_unicode(self):
"""
To prevent almost identical usernames, visually identical but differing
diff -Nru python-django-3.2.19/tests/forms_tests/field_tests/test_emailfield.py python-django-3.2.25/tests/forms_tests/field_tests/test_emailfield.py
--- python-django-3.2.19/tests/forms_tests/field_tests/test_emailfield.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/tests/forms_tests/field_tests/test_emailfield.py 2024-03-04 07:47:52.000000000 +0000
@@ -9,7 +9,10 @@
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 -Nru python-django-3.2.19/tests/forms_tests/field_tests/test_filefield.py python-django-3.2.25/tests/forms_tests/field_tests/test_filefield.py
--- python-django-3.2.19/tests/forms_tests/field_tests/test_filefield.py 2023-05-03 11:58:44.000000000 +0000
+++ python-django-3.2.25/tests/forms_tests/field_tests/test_filefield.py 2024-03-04 07:47:52.000000000 +0000
@@ -1,4 +1,5 @@
import pickle
+import unittest
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -6,6 +7,13 @@
from django.forms import FileField, FileInput
from django.test import SimpleTestCase
+try:
+ from PIL import Image # NOQA
+except ImportError:
+ HAS_PILLOW = False
+else:
+ HAS_PILLOW = True
+
class FileFieldTest(SimpleTestCase):
@@ -125,6 +133,7 @@
with self.assertRaisesMessage(ValidationError, msg):
f.clean(files[::-1])
+ @unittest.skipUnless(HAS_PILLOW, "Pillow not installed")
def test_file_multiple_validation(self):
f = MultipleFileField(validators=[validate_image_file_extension])
diff -Nru python-django-3.2.19/tests/forms_tests/tests/test_forms.py python-django-3.2.25/tests/forms_tests/tests/test_forms.py
--- python-django-3.2.19/tests/forms_tests/tests/test_forms.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/tests/forms_tests/tests/test_forms.py 2024-03-04 07:47:52.000000000 +0000
@@ -422,11 +422,18 @@
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 @@
Yes
No
-Email:
+Email:
Age: """
)
@@ -2840,7 +2847,7 @@
Yes
No
-Email:
+Email:
Age:
"""
@@ -2859,7 +2866,7 @@
No
Email:
-
+
Age:
"""
@@ -3489,7 +3496,7 @@
f = CommentForm(data, auto_id=False, error_class=DivErrorList)
self.assertHTMLEqual(f.as_p(), """Name:
Enter a valid email address.
-Email:
+Email:
Comment:
""")
diff -Nru python-django-3.2.19/tests/humanize_tests/tests.py python-django-3.2.25/tests/humanize_tests/tests.py
--- python-django-3.2.19/tests/humanize_tests/tests.py 2023-05-03 11:57:57.000000000 +0000
+++ python-django-3.2.25/tests/humanize_tests/tests.py 2024-03-04 07:47:52.000000000 +0000
@@ -66,28 +66,156 @@
def test_intcomma(self):
test_list = (
- 100, 1000, 10123, 10311, 1000000, 1234567.25, '100', '1000',
- '10123', '10311', '1000000', '1234567.1234567',
- Decimal('1234567.1234567'), None,
+ 100,
+ -100,
+ 1000,
+ -1000,
+ 10123,
+ -10123,
+ 10311,
+ -10311,
+ 1000000,
+ -1000000,
+ 1234567.25,
+ -1234567.25,
+ "100",
+ "-100",
+ "100.1",
+ "-100.1",
+ "100.13",
+ "-100.13",
+ "1000",
+ "-1000",
+ "10123",
+ "-10123",
+ "10311",
+ "-10311",
+ "100000.13",
+ "-100000.13",
+ "1000000",
+ "-1000000",
+ "1234567.1234567",
+ "-1234567.1234567",
+ Decimal("1234567.1234567"),
+ Decimal("-1234567.1234567"),
+ None,
+ "1234567",
+ "-1234567",
+ "1234567.12",
+ "-1234567.12",
+ "the quick brown fox jumped over the lazy dog",
)
result_list = (
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
- '1,234,567.1234567', None,
+ "100",
+ "-100",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.25",
+ "-1,234,567.25",
+ "100",
+ "-100",
+ "100.1",
+ "-100.1",
+ "100.13",
+ "-100.13",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "100,000.13",
+ "-100,000.13",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ None,
+ "1,234,567",
+ "-1,234,567",
+ "1,234,567.12",
+ "-1,234,567.12",
+ "the quick brown fox jumped over the lazy dog",
)
with translation.override('en'):
self.humanize_tester(test_list, result_list, 'intcomma')
def test_l10n_intcomma(self):
test_list = (
- 100, 1000, 10123, 10311, 1000000, 1234567.25, '100', '1000',
- '10123', '10311', '1000000', '1234567.1234567',
- Decimal('1234567.1234567'), None,
+ 100,
+ -100,
+ 1000,
+ -1000,
+ 10123,
+ -10123,
+ 10311,
+ -10311,
+ 1000000,
+ -1000000,
+ 1234567.25,
+ -1234567.25,
+ "100",
+ "-100",
+ "1000",
+ "-1000",
+ "10123",
+ "-10123",
+ "10311",
+ "-10311",
+ "1000000",
+ "-1000000",
+ "1234567.1234567",
+ "-1234567.1234567",
+ Decimal("1234567.1234567"),
+ -Decimal("1234567.1234567"),
+ None,
+ "1234567",
+ "-1234567",
+ "1234567.12",
+ "-1234567.12",
+ "the quick brown fox jumped over the lazy dog",
)
result_list = (
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.25',
- '100', '1,000', '10,123', '10,311', '1,000,000', '1,234,567.1234567',
- '1,234,567.1234567', None,
+ "100",
+ "-100",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.25",
+ "-1,234,567.25",
+ "100",
+ "-100",
+ "1,000",
+ "-1,000",
+ "10,123",
+ "-10,123",
+ "10,311",
+ "-10,311",
+ "1,000,000",
+ "-1,000,000",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ "1,234,567.1234567",
+ "-1,234,567.1234567",
+ None,
+ "1,234,567",
+ "-1,234,567",
+ "1,234,567.12",
+ "-1,234,567.12",
+ "the quick brown fox jumped over the lazy dog",
)
with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=False):
with translation.override('en'):
diff -Nru python-django-3.2.19/tests/requirements/py3.txt python-django-3.2.25/tests/requirements/py3.txt
--- python-django-3.2.19/tests/requirements/py3.txt 2023-05-03 11:57:58.000000000 +0000
+++ python-django-3.2.25/tests/requirements/py3.txt 2024-03-04 07:47:52.000000000 +0000
@@ -12,7 +12,7 @@
pylibmc; sys.platform != 'win32'
pymemcache >= 3.4.0
# RemovedInDjango41Warning.
-python-memcached >= 1.59
+python-memcached == 1.59
pytz
pywatchman; sys.platform != 'win32'
PyYAML
diff -Nru python-django-3.2.19/tests/utils_tests/test_encoding.py python-django-3.2.25/tests/utils_tests/test_encoding.py
--- python-django-3.2.19/tests/utils_tests/test_encoding.py 2023-05-03 11:57:58.000000000 +0000
+++ python-django-3.2.25/tests/utils_tests/test_encoding.py 2024-03-04 07:47:52.000000000 +0000
@@ -1,9 +1,10 @@
import datetime
+import inspect
import sys
import unittest
from pathlib import Path
from unittest import mock
-from urllib.parse import quote_plus
+from urllib.parse import quote, quote_plus
from django.test import SimpleTestCase
from django.utils.encoding import (
@@ -101,6 +102,24 @@
except RecursionError:
self.fail('Unexpected RecursionError raised.')
+ def test_repercent_broken_unicode_small_fragments(self):
+ data = b"test\xfctest\xfctest\xfc"
+ decoded_paths = []
+
+ def mock_quote(*args, **kwargs):
+ # The second frame is the call to repercent_broken_unicode().
+ decoded_paths.append(inspect.currentframe().f_back.f_locals["path"])
+ return quote(*args, **kwargs)
+
+ with mock.patch("django.utils.encoding.quote", mock_quote):
+ self.assertEqual(repercent_broken_unicode(data), b"test%FCtest%FCtest%FC")
+
+ # decode() is called on smaller fragment of the path each time.
+ self.assertEqual(
+ decoded_paths,
+ [b"test\xfctest\xfctest\xfc", b"test\xfctest\xfc", b"test\xfc"],
+ )
+
class TestRFC3987IEncodingUtils(unittest.TestCase):
diff -Nru python-django-3.2.19/tests/utils_tests/test_text.py python-django-3.2.25/tests/utils_tests/test_text.py
--- python-django-3.2.19/tests/utils_tests/test_text.py 2023-05-03 11:57:58.000000000 +0000
+++ python-django-3.2.25/tests/utils_tests/test_text.py 2024-03-04 07:47:52.000000000 +0000
@@ -1,5 +1,6 @@
import json
import sys
+from unittest.mock import patch
from django.core.exceptions import SuspiciousFileOperation
from django.test import SimpleTestCase, ignore_warnings
@@ -90,11 +91,17 @@
# lazy strings are handled correctly
self.assertEqual(text.Truncator(lazystr('The quick brown fox')).chars(10), 'The quick…')
- def test_truncate_chars_html(self):
+ @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
+ def test_truncate_chars_html_size_limit(self):
+ max_len = text.Truncator.MAX_LENGTH_HTML
+ bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
+ valid_html = "Joel is a slug
" # 14 chars
perf_test_values = [
- ((' ', None),
- ('&' * 50000, '&' * 9 + '…'),
- ('_X<<<<<<<<<<<>', None),
+ (" ", None),
+ ("", "", None),
+ (valid_html * bigger_len, "Joel is a…
"), # 10 chars
]
for value, expected in perf_test_values:
with self.subTest(value=value):
@@ -152,15 +159,51 @@
truncator = text.Truncator('I <3 python, what about you?
')
self.assertEqual('I <3 python,…
', truncator.words(3, html=True))
+ # Only open brackets.
+ test = "<" * 60_000
+ truncator = text.Truncator(test)
+ self.assertEqual(truncator.words(1, html=True), test)
+
+ # Tags with special chars in attrs.
+ truncator = text.Truncator(
+ """Hello, my dear lady! """
+ )
+ self.assertEqual(
+ """Hello, my dear… """,
+ truncator.words(3, html=True),
+ )
+
+ # Tags with special non-latin chars in attrs.
+ truncator = text.Truncator("""Hello, my dear lady!
""")
+ self.assertEqual(
+ """Hello, my dear…
""",
+ truncator.words(3, html=True),
+ )
+
+ # Misplaced brackets.
+ truncator = text.Truncator("hello >< world")
+ self.assertEqual(truncator.words(1, html=True), "hello…")
+ self.assertEqual(truncator.words(2, html=True), "hello >< world")
+
+ @patch("django.utils.text.Truncator.MAX_LENGTH_HTML", 10_000)
+ def test_truncate_words_html_size_limit(self):
+ max_len = text.Truncator.MAX_LENGTH_HTML
+ bigger_len = text.Truncator.MAX_LENGTH_HTML + 1
+ valid_html = "Joel is a slug
" # 4 words
perf_test_values = [
- ('',
- '&' * 50000,
- '_X<<<<<<<<<<<>',
+ ("", None),
+ ("", "", None),
+ (valid_html * bigger_len, valid_html * 12 + "Joel is…
"), # 50 words
]
- for value in perf_test_values:
+ for value, expected in perf_test_values:
with self.subTest(value=value):
truncator = text.Truncator(value)
- self.assertEqual(value, truncator.words(50, html=True))
+ self.assertEqual(
+ expected if expected else value, truncator.words(50, html=True)
+ )
def test_wrap(self):
digits = '1234 67 9'
diff -Nru python-django-3.2.19/tests/validators/tests.py python-django-3.2.25/tests/validators/tests.py
--- python-django-3.2.19/tests/validators/tests.py 2023-05-03 11:57:58.000000000 +0000
+++ python-django-3.2.25/tests/validators/tests.py 2024-03-04 07:47:52.000000000 +0000
@@ -59,6 +59,7 @@
(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 @@
(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),