Compare commits

...

10 Commits

Author SHA1 Message Date
openeuler-ci-bot
5efc447151
!169 [sync] PR-162: Fix CVE-2025-32873
From: @openeuler-sync-bot 
Reviewed-by: @cherry530 
Signed-off-by: @cherry530
2025-05-09 06:11:17 +00:00
starlet-dx
50ada0b203 Fix CVE-2025-32873
(cherry picked from commit 97f56e17d550c32bedd01e9e8963207f4b4c2f23)
2025-05-09 10:57:13 +08:00
openeuler-ci-bot
e6d95e8b85
!155 [sync] PR-152: fix CVE-2025-26699
From: @openeuler-sync-bot 
Reviewed-by: @cherry530 
Signed-off-by: @cherry530
2025-03-12 03:10:42 +00:00
changtao
39c60f2c69 fix CVE-2025-26699
(cherry picked from commit b619d2140f100cfb464289449d11994907953c61)
2025-03-12 09:46:33 +08:00
openeuler-ci-bot
b2d519316c
!145 [sync] PR-144: Fix CVE-2024-56374
From: @openeuler-sync-bot 
Reviewed-by: @cherry530 
Signed-off-by: @cherry530
2025-01-17 02:30:20 +00:00
starlet-dx
885a379e15 Fix CVE-2024-56374
(cherry picked from commit 0c7de1a43d59ed3d933d3dc51e69a6917977a0bf)
2025-01-17 09:36:14 +08:00
openeuler-ci-bot
ebba19d2c5
!143 [sync] PR-137: Fix CVE-2024-53907 CVE-2024-53908
From: @openeuler-sync-bot 
Reviewed-by: @cherry530 
Signed-off-by: @cherry530
2024-12-09 06:39:09 +00:00
wk333
8e53943a53 Fix CVE-2024-53907 CVE-2024-53908
(cherry picked from commit 66ebce54912f2aee338790ac1090d3fb018a8902)
2024-12-09 11:00:07 +08:00
openeuler-ci-bot
0b141b58df
!129 Fix CVE-2024-45230 CVE-2024-45231
From: @zhangxianting 
Reviewed-by: @cherry530 
Signed-off-by: @cherry530
2024-10-11 02:41:11 +00:00
zhangxianting
2f48f14416 Fix CVE-2024-45230 CVE-2024-45231 2024-10-10 15:18:40 +08:00
8 changed files with 1024 additions and 1 deletions

135
CVE-2024-45230.patch Normal file
View File

@ -0,0 +1,135 @@
From d147a8ebbdf28c17cafbbe2884f0bc57e2bf82e2 Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Mon, 12 Aug 2024 15:17:57 +0200
Subject: [PATCH] [4.2.x] Fixed CVE-2024-45230 -- Mitigated potential DoS in
urlize and urlizetrunc template filters.
Thanks MProgrammer (https://hackerone.com/mprogrammer) for the report.
---
django/utils/html.py | 17 ++++++++------
docs/ref/templates/builtins.txt | 11 ++++++++++
docs/releases/4.2.16.txt | 14 ++++++++++++
.../filter_tests/test_urlize.py | 22 +++++++++++++++++++
tests/utils_tests/test_html.py | 1 +
5 files changed, 58 insertions(+), 7 deletions(-)
create mode 100644 docs/releases/4.2.16.txt
diff --git a/django/utils/html.py b/django/utils/html.py
index 23575d3..df38c20 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -395,14 +395,17 @@ class Urlizer:
potential_entity = middle[amp:]
escaped = html.unescape(potential_entity)
if escaped == potential_entity or escaped.endswith(";"):
- rstripped = middle.rstrip(";")
- amount_stripped = len(middle) - len(rstripped)
- if amp > -1 and amount_stripped > 1:
- # Leave a trailing semicolon as might be an entity.
- trail = middle[len(rstripped) + 1 :] + trail
- middle = rstripped + ";"
+ rstripped = middle.rstrip(self.trailing_punctuation_chars)
+ trail_start = len(rstripped)
+ amount_trailing_semicolons = len(middle) - len(middle.rstrip(";"))
+ if amp > -1 and amount_trailing_semicolons > 1:
+ # Leave up to most recent semicolon as might be an entity.
+ recent_semicolon = middle[trail_start:].index(";")
+ middle_semicolon_index = recent_semicolon + trail_start + 1
+ trail = middle[middle_semicolon_index:] + trail
+ middle = rstripped + middle[trail_start:middle_semicolon_index]
else:
- trail = middle[len(rstripped) :] + trail
+ trail = middle[trail_start:] + trail
middle = rstripped
trimmed_something = True
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 39aa398..dda5b42 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -2831,6 +2831,17 @@ Django's built-in :tfilter:`escape` filter. The default value for
email addresses that contain single quotes (``'``), things won't work as
expected. Apply this filter only to plain text.
+.. warning::
+
+ Using ``urlize`` or ``urlizetrunc`` can incur a performance penalty, which
+ can become severe when applied to user controlled values such as content
+ stored in a :class:`~django.db.models.TextField`. You can use
+ :tfilter:`truncatechars` to add a limit to such inputs:
+
+ .. code-block:: html+django
+
+ {{ value|truncatechars:500|urlize }}
+
.. templatefilter:: urlizetrunc
``urlizetrunc``
diff --git a/docs/releases/4.2.16.txt b/docs/releases/4.2.16.txt
new file mode 100644
index 0000000..b624d5c
--- /dev/null
+++ b/docs/releases/4.2.16.txt
@@ -0,0 +1,14 @@
+===========================
+Django 4.2.16 release notes
+===========================
+*September 3, 2024*
+Django 4.2.16 fixes one security issue with severity "moderate" and one
+security issue with severity "low" in 4.2.15.
+
+...
+CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html.urlize()``
+===========================================================================================
+
+:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
+denial-of-service attack via very large inputs with a specific sequence of
+characters.
diff --git a/tests/template_tests/filter_tests/test_urlize.py b/tests/template_tests/filter_tests/test_urlize.py
index abc227b..e542802 100644
--- a/tests/template_tests/filter_tests/test_urlize.py
+++ b/tests/template_tests/filter_tests/test_urlize.py
@@ -305,6 +305,28 @@ class FunctionTests(SimpleTestCase):
"http://testing.com/example</a>.,:;)&quot;!",
)
+ def test_trailing_semicolon(self):
+ self.assertEqual(
+ urlize("http://example.com?x=&amp;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp;</a>",
+ )
+ self.assertEqual(
+ urlize("http://example.com?x=&amp;;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp;</a>;",
+ )
+ self.assertEqual(
+ urlize("http://example.com?x=&amp;;;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp;</a>;;",
+ )
+ self.assertEqual(
+ urlize("http://example.com?x=&amp.;...;", autoescape=False),
+ '<a href="http://example.com?x=" rel="nofollow">'
+ "http://example.com?x=&amp</a>.;...;",
+ )
+
def test_brackets(self):
"""
#19070 - Check urlize handles brackets properly
diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py
index 83ebe43..7ff5020 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -364,6 +364,7 @@ class TestUtilsHtml(SimpleTestCase):
"&:" + ";" * 100_000,
"&.;" * 100_000,
".;" * 100_000,
+ "&" + ";:" * 100_000,
)
for value in tests:
with self.subTest(value=value):
--
2.43.0

159
CVE-2024-45231.patch Normal file
View File

@ -0,0 +1,159 @@
From bf4888d317ba4506d091eeac6e8b4f1fcc731199 Mon Sep 17 00:00:00 2001
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/ref/logging.txt | 12 ++++++++++++
docs/releases/4.2.16.txt | 11 +++++++++++
docs/topics/auth/default.txt | 4 +++-
tests/auth_tests/test_forms.py | 21 +++++++++++++++++++++
tests/mail/custombackend.py | 5 +++++
6 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 061dc81b42..7f85787f03 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
@@ -16,6 +17,7 @@ from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
UserModel = get_user_model()
+logger = logging.getLogger("django.contrib.auth")
def _unicode_ci_compare(s1, s2):
@@ -314,7 +316,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/ref/logging.txt b/docs/ref/logging.txt
index b11fb752f7..3d33e0af63 100644
--- a/docs/ref/logging.txt
+++ b/docs/ref/logging.txt
@@ -204,6 +204,18 @@ all database queries.
Support for logging transaction management queries (``BEGIN``, ``COMMIT``,
and ``ROLLBACK``) was added.
+.. _django-contrib-auth-logger:
+
+``django.contrib.auth``
+~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 4.2.16
+
+Log messages related to :doc:`contrib/auth`, particularly ``ERROR`` messages
+are generated when a :class:`~django.contrib.auth.forms.PasswordResetForm` is
+successfully submitted but the password reset email cannot be delivered due to
+a mail sending exception.
+
.. _django-security-logger:
``django.security.*``
diff --git a/docs/releases/4.2.16.txt b/docs/releases/4.2.16.txt
index 2a84186867..963036345c 100644
--- a/docs/releases/4.2.16.txt
+++ b/docs/releases/4.2.16.txt
@@ -13,3 +13,14 @@ CVE-2024-45230: Potential denial-of-service vulnerability in ``django.utils.html
:tfilter:`urlize` and :tfilter:`urlizetrunc` were subject to a potential
denial-of-service attack via very large inputs with a specific sequence of
characters.
+
+CVE-2024-45231: Potential user email enumeration via response status on password reset
+======================================================================================
+
+Due to unhandled email sending failures, the
+:class:`~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 :ref:`django-contrib-auth-logger` logger.
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index 528902416d..ad840c5e57 100644
--- a/docs/topics/auth/default.txt
+++ b/docs/topics/auth/default.txt
@@ -1661,7 +1661,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 81c56a428e..f068d347a9 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -1245,6 +1245,27 @@ class PasswordResetFormTest(TestDataMixin, TestCase):
)
)
+ @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 14e7f077ba..c6c567b642 100644
--- a/tests/mail/custombackend.py
+++ b/tests/mail/custombackend.py
@@ -12,3 +12,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.")
--
2.20.1

88
CVE-2024-53907.patch Normal file
View File

@ -0,0 +1,88 @@
From 790eb058b0716c536a2f2e8d1c6d5079d776c22b Mon Sep 17 00:00:00 2001
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().
Origin: https://github.com/django/django/commit/790eb058b0716c536a2f2e8d1c6d5079d776c22b
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 +++++++
3 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/django/utils/html.py b/django/utils/html.py
index df38c2051994..a3a7238cba44 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -6,6 +6,7 @@
from html.parser import HTMLParser
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, cached_property, keep_lazy, keep_lazy_text
from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS
@@ -14,6 +15,7 @@
from django.utils.text import normalize_newlines
MAX_URL_LENGTH = 2048
+MAX_STRIP_TAGS_DEPTH = 50
@keep_lazy(SafeString)
@@ -172,15 +174,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 7ff5020fb6d3..579bb2a1e359 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.core.serializers.json import DjangoJSONEncoder
from django.test import SimpleTestCase
from django.utils.functional import lazystr
@@ -113,12 +114,18 @@ def test_strip_tags(self):
("<script>alert()</script>&h", "alert()h"),
("><!" + ("&" * 16000) + "D", "><!" + ("&" * 16000) + "D"),
("X<<<<br>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, "<a>"
+ 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"):

145
CVE-2024-53908.patch Normal file
View File

@ -0,0 +1,145 @@
From 7376bcbf508883282ffcc0f0fac5cf0ed2d6cbc5 Mon Sep 17 00:00:00 2001
From: Simon Charette <charette.s@gmail.com>
Date: Fri, 8 Nov 2024 21:27:31 -0500
Subject: [PATCH] [4.2.x] Fixed CVE-2024-53908 -- Prevented SQL injections in
direct HasKeyLookup usage on Oracle.
Origin: https://github.com/django/django/commit/7376bcbf508883282ffcc0f0fac5cf0ed2d6cbc5
Thanks Seokchan Yoon for the report, and Mariusz Felisiak and Sarah
Boyce for the reviews.
---
django/db/models/fields/json.py | 53 ++++++++++++++++++----------
tests/model_fields/test_jsonfield.py | 9 +++++
3 files changed, 53 insertions(+), 18 deletions(-)
diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py
index b7cde157c4fa..b9c6ff1752b9 100644
--- a/django/db/models/fields/json.py
+++ b/django/db/models/fields/json.py
@@ -216,20 +216,18 @@ def compile_json_path_final_key(self, key_transform):
# Compile the final key without interpreting ints as array elements.
return ".%s" % json.dumps(key_transform)
- def as_sql(self, compiler, connection, template=None):
+ def _as_sql_parts(self, compiler, connection):
# Process JSON path from the left-hand side.
if isinstance(self.lhs, KeyTransform):
- lhs, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs(
+ lhs_sql, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs(
compiler, connection
)
lhs_json_path = compile_json_path(lhs_key_transforms)
else:
- lhs, lhs_params = self.process_lhs(compiler, connection)
+ lhs_sql, lhs_params = self.process_lhs(compiler, connection)
lhs_json_path = "$"
- sql = template % lhs
# Process JSON path from the right-hand side.
rhs = self.rhs
- rhs_params = []
if not isinstance(rhs, (list, tuple)):
rhs = [rhs]
for key in rhs:
@@ -240,24 +238,43 @@ def as_sql(self, compiler, connection, template=None):
*rhs_key_transforms, final_key = rhs_key_transforms
rhs_json_path = compile_json_path(rhs_key_transforms, include_root=False)
rhs_json_path += self.compile_json_path_final_key(final_key)
- rhs_params.append(lhs_json_path + rhs_json_path)
+ yield lhs_sql, lhs_params, lhs_json_path + rhs_json_path
+
+ def _combine_sql_parts(self, parts):
# Add condition for each key.
if self.logical_operator:
- sql = "(%s)" % self.logical_operator.join([sql] * len(rhs_params))
- return sql, tuple(lhs_params) + tuple(rhs_params)
+ return "(%s)" % self.logical_operator.join(parts)
+ return "".join(parts)
+
+ def as_sql(self, compiler, connection, template=None):
+ sql_parts = []
+ params = []
+ for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts(
+ compiler, connection
+ ):
+ sql_parts.append(template % (lhs_sql, "%s"))
+ params.extend(lhs_params + [rhs_json_path])
+ return self._combine_sql_parts(sql_parts), tuple(params)
def as_mysql(self, compiler, connection):
return self.as_sql(
- compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %%s)"
+ compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %s)"
)
def as_oracle(self, compiler, connection):
- sql, params = self.as_sql(
- compiler, connection, template="JSON_EXISTS(%s, '%%s')"
- )
- # Add paths directly into SQL because path expressions cannot be passed
- # as bind variables on Oracle.
- return sql % tuple(params), []
+ template = "JSON_EXISTS(%s, '%s')"
+ sql_parts = []
+ params = []
+ for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts(
+ compiler, connection
+ ):
+ # Add right-hand-side directly into SQL because it cannot be passed
+ # as bind variables to JSON_EXISTS. It might result in invalid
+ # queries but it is assumed that it cannot be evaded because the
+ # path is JSON serialized.
+ sql_parts.append(template % (lhs_sql, rhs_json_path))
+ params.extend(lhs_params)
+ return self._combine_sql_parts(sql_parts), tuple(params)
def as_postgresql(self, compiler, connection):
if isinstance(self.rhs, KeyTransform):
@@ -269,7 +286,7 @@ def as_postgresql(self, compiler, connection):
def as_sqlite(self, compiler, connection):
return self.as_sql(
- compiler, connection, template="JSON_TYPE(%s, %%s) IS NOT NULL"
+ compiler, connection, template="JSON_TYPE(%s, %s) IS NOT NULL"
)
@@ -467,9 +484,9 @@ def as_oracle(self, compiler, connection):
return "(NOT %s OR %s IS NULL)" % (sql, lhs), tuple(params) + tuple(lhs_params)
def as_sqlite(self, compiler, connection):
- template = "JSON_TYPE(%s, %%s) IS NULL"
+ template = "JSON_TYPE(%s, %s) IS NULL"
if not self.rhs:
- template = "JSON_TYPE(%s, %%s) IS NOT NULL"
+ template = "JSON_TYPE(%s, %s) IS NOT NULL"
return HasKeyOrArrayIndex(self.lhs.lhs, self.lhs.key_name).as_sql(
compiler,
connection,
diff --git a/tests/model_fields/test_jsonfield.py b/tests/model_fields/test_jsonfield.py
index 4a1cc075b4c4..4c8d14bf9a17 100644
--- a/tests/model_fields/test_jsonfield.py
+++ b/tests/model_fields/test_jsonfield.py
@@ -29,6 +29,7 @@
from django.db.models.expressions import RawSQL
from django.db.models.fields.json import (
KT,
+ HasKey,
KeyTextTransform,
KeyTransform,
KeyTransformFactory,
@@ -607,6 +608,14 @@ def test_has_key_deep(self):
[expected],
)
+ def test_has_key_literal_lookup(self):
+ self.assertSequenceEqual(
+ NullableJSONModel.objects.filter(
+ HasKey(Value({"foo": "bar"}, JSONField()), "foo")
+ ).order_by("id"),
+ self.objs,
+ )
+
def test_has_key_list(self):
obj = NullableJSONModel.objects.create(value=[{"a": 1}, {"b": "x"}])
tests = [

284
CVE-2024-56374.patch Normal file
View File

@ -0,0 +1,284 @@
From ad866a1ca3e7d60da888d25d27e46a8adb2ed36e Mon Sep 17 00:00:00 2001
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>
Origin: https://github.com/django/django/commit/ad866a1ca3e7d60da888d25d27e46a8adb2ed36e
---
django/db/models/fields/__init__.py | 6 +--
django/forms/fields.py | 7 +++-
django/utils/ipv6.py | 19 +++++++--
docs/ref/forms/fields.txt | 13 +++++-
.../field_tests/test_genericipaddressfield.py | 33 ++++++++++++++-
tests/utils_tests/test_ipv6.py | 40 +++++++++++++++++--
6 files changed, 104 insertions(+), 14 deletions(-)
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index b65948d..0cfba4e 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -25,7 +25,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 _
@@ -2160,7 +2160,7 @@ class GenericIPAddressField(Field):
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):
@@ -2187,7 +2187,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") == self.max_length:
del kwargs["max_length"]
return name, path, args, kwargs
diff --git a/django/forms/fields.py b/django/forms/fields.py
index 01cd831..e62417f 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -42,7 +42,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 _
from django.utils.translation import ngettext_lazy
@@ -1284,6 +1284,7 @@ class GenericIPAddressField(CharField):
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):
@@ -1291,7 +1292,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 88dd6ec..de41a97 100644
--- a/django/utils/ipv6.py
+++ b/django/utils/ipv6.py
@@ -3,9 +3,22 @@ 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.")
+ ip_str,
+ unpack_ipv4=False,
+ error_message=_("This is not a valid IPv6 address."),
+ max_length=MAX_IPV6_ADDRESS_LENGTH,
):
"""
Clean an IPv6 address string.
@@ -24,7 +37,7 @@ def clean_ipv6_address(
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")
@@ -41,7 +54,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 1a7274e..76b4587 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -719,7 +719,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
@@ -727,7 +727,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
@@ -742,6 +742,15 @@ For each field, we describe the default widget used if you don't specify
``192.0.2.1``. Default is disabled. Can only be used
when ``protocol`` is set to ``'both'``.
+ .. attribute:: max_length
+
+ Defaults to 39, and behaves the same way as it does for
+ :class:`CharField`.
+
+ .. versionchanged:: 4.2.18
+
+ The default value for ``max_length`` was set to 39 characters.
+
``ImageField``
--------------
diff --git a/tests/forms_tests/field_tests/test_genericipaddressfield.py b/tests/forms_tests/field_tests/test_genericipaddressfield.py
index 80722f5..ef00a72 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):
@@ -125,6 +126,35 @@ class GenericIPAddressFieldTest(SimpleTestCase):
):
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(""), "")
@@ -150,7 +180,8 @@ class GenericIPAddressFieldTest(SimpleTestCase):
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"
+ 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.'"
diff --git a/tests/utils_tests/test_ipv6.py b/tests/utils_tests/test_ipv6.py
index bf78ed9..2d06507 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"))
self.assertTrue(is_valid_ipv6_address("2a02::223:6cff:fe8a:2e8a"))
@@ -64,3 +72,29 @@ class TestUtilsIPv6(unittest.TestCase):
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())
--
2.46.0

85
CVE-2025-32873.patch Normal file
View File

@ -0,0 +1,85 @@
From 9cd8028f3e38dca8e51c1388f474eecbe7d6ca3c Mon Sep 17 00:00:00 2001
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
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.
Origin: https://github.com/django/django/commit/9cd8028f3e38dca8e51c1388f474eecbe7d6ca3c
---
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 a3a7238..84c37d1 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -17,6 +17,9 @@ from django.utils.text import normalize_newlines
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(SafeString)
def escape(text):
@@ -175,6 +178,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 579bb2a..25168e2 100644
--- a/tests/utils_tests/test_html.py
+++ b/tests/utils_tests/test_html.py
@@ -115,17 +115,30 @@ class TestUtilsHtml(SimpleTestCase):
("><!" + ("&" * 16000) + "D", "><!" + ("&" * 16000) + "D"),
("X<<<<br>br>br>br>X", "XX"),
("<" * 50 + "a>" * 50, ""),
+ (">" + "<a" * 500 + "a", ">" + "<a" * 500 + "a"),
+ ("<a" * 49 + "a" * 951, "<a" * 49 + "a" * 951),
+ ("<" + "a" * 1_002, "<" + "a" * 1_002),
)
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):
+ def test_strip_tags_suspicious_operation_max_depth(self):
value = "<" * 51 + "a>" * 51, "<a>"
with self.assertRaises(SuspiciousOperation):
strip_tags(value)
+ def test_strip_tags_suspicious_operation_large_open_tags(self):
+ items = [
+ ">" + "<a" * 501,
+ "<a" * 50 + "a" * 950,
+ ]
+ for value in items:
+ with self.subTest(value=value):
+ 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"):
--
2.49.0

View File

@ -0,0 +1,102 @@
From 4f2765232336b8ad0afd8017d9d912ae93470017 Mon Sep 17 00:00:00 2001
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
Date: Tue, 25 Feb 2025 09:40:54 +0100
Subject: [PATCH] [5.0.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 | 28 +++++++------------
docs/releases/4.2.15.txt | 7 +++++
.../filter_tests/test_wordwrap.py | 12 ++++++++
3 files changed, 29 insertions(+), 18 deletions(-)
diff --git a/django/utils/text.py b/django/utils/text.py
index e1b835e..81ae88d 100644
--- a/django/utils/text.py
+++ b/django/utils/text.py
@@ -1,6 +1,7 @@
import gzip
import re
import secrets
+import textwrap
import unicodedata
from gzip import GzipFile
from gzip import compress as gzip_compress
@@ -97,24 +98,15 @@ def wrap(text, width):
``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/docs/releases/4.2.15.txt b/docs/releases/4.2.15.txt
index b1d4684..4ee3882 100644
--- a/docs/releases/4.2.15.txt
+++ b/docs/releases/4.2.15.txt
@@ -7,6 +7,13 @@ Django 4.2.15 release notes
Django 4.2.15 fixes three security issues with severity "moderate", one
security issue with severity "high", and a regression in 4.2.14.
+
+CVE-2025-26699: Potential denial-of-service vulnerability in ``django.utils.text.wrap()``
+=========================================================================================
+
+The ``wrap()`` and :tfilter:`wordwrap` template filter were subject to a
+potential denial-of-service attack when used with very long strings.
+
CVE-2024-41989: Memory exhaustion in ``django.utils.numberformat.floatformat()``
================================================================================
diff --git a/tests/template_tests/filter_tests/test_wordwrap.py b/tests/template_tests/filter_tests/test_wordwrap.py
index 88fbd27..7557153 100644
--- a/tests/template_tests/filter_tests/test_wordwrap.py
+++ b/tests/template_tests/filter_tests/test_wordwrap.py
@@ -78,3 +78,15 @@ class FunctionTests(SimpleTestCase):
"this is a long\nparagraph of\ntext that\nreally needs\nto be wrapped\n"
"I'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),
+ )
--
2.46.0

View File

@ -1,11 +1,18 @@
%global _empty_manifest_terminate_build 0
Name: python-django
Version: 4.2.15
Release: 1
Release: 6
Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design.
License: Apache-2.0 and Python-2.0 and BSD-3-Clause
URL: https://www.djangoproject.com/
Source0: https://files.pythonhosted.org/packages/source/d/Django/Django-%{version}.tar.gz
Patch0: CVE-2024-45230.patch
Patch1: CVE-2024-45231.patch
Patch2: CVE-2024-53907.patch
Patch3: CVE-2024-53908.patch
Patch4: CVE-2024-56374.patch
Patch5: backport-CVE-2025-26699.patch
Patch6: CVE-2025-32873.patch
BuildArch: noarch
%description
@ -72,6 +79,24 @@ mv %{buildroot}/doclist.lst .
%{_docdir}/*
%changelog
* Fri May 09 2025 yaoxin <1024769339@qq.com> - 4.2.15-6
- Fix CVE-2025-32873
* Mon Mar 10 2025 changtao <changtao@kylinos.cn> - 4.2.15-5
- Type:CVE
- CVE:CVE-2025-26699
- SUG:NA
- DESC:fix CVE-2025-26699
* Fri Jan 17 2025 yaoxin <1024769339@qq.com> - 4.2.15-4
- Fix CVE-2024-56374
* Mon Dec 09 2024 wangkai <13474090681@163.com> - 4.2.15-3
- Fix CVE-2024-53907 CVE-2024-53908
* Thu Oct 10 2024 zhangxianting <zhangxianting@uniontech.com> - 4.2.15-2
- Fix CVE-2024-45230 CVE-2024-45231
* Thu Aug 08 2024 yaoxin <yao_xin001@hoperun.com> - 4.2.15-1
- Update to 4.2.15
* CVE-2024-41989: Memory exhaustion in ``django.utils.numberformat.floatformat()``