diff --git a/python-django-5.2.7-CVE-2025-13372.patch b/python-django-5.2.7-CVE-2025-13372.patch deleted file mode 100644 index e2ca3248a6c7998143298332e8f07e89e8572366..0000000000000000000000000000000000000000 --- a/python-django-5.2.7-CVE-2025-13372.patch +++ /dev/null @@ -1,65 +0,0 @@ -From 479415ce5249bcdebeb6570c72df2a87f45a7bbf Mon Sep 17 00:00:00 2001 -From: Jacob Walls -Date: Mon, 17 Nov 2025 17:09:54 -0500 -Subject: [PATCH] [5.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 | 11 ++++++++++- - tests/annotations/tests.py | 11 +++++++++++ - 5 files changed, 45 insertions(+), 1 deletion(-) - -diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py -index dc2db148aed5..38b61c489838 100644 ---- a/django/db/backends/postgresql/compiler.py -+++ b/django/db/backends/postgresql/compiler.py -@@ -1,6 +1,6 @@ - from django.db.models.sql.compiler import ( - SQLAggregateCompiler, -- SQLCompiler, -+ SQLCompiler as BaseSQLCompiler, - SQLDeleteCompiler, - ) - from django.db.models.sql.compiler import SQLInsertCompiler as BaseSQLInsertCompiler -@@ -25,6 +25,15 @@ def __str__(self): - return "UNNEST(%s)" % ", ".join(self) - - -+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) -+ -+ - class SQLInsertCompiler(BaseSQLInsertCompiler): - def assemble_as_sql(self, fields, value_rows): - # Specialize bulk-insertion of literal values through UNNEST to - -diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py -index 7a1212122427..78e5408d0fe7 100644 ---- a/tests/annotations/tests.py -+++ b/tests/annotations/tests.py -@@ -1507,3 +1507,14 @@ def test_alias_filtered_relation_sql_injection(self): - ) - with self.assertRaisesMessage(ValueError, msg): - Book.objects.alias(**{crafted_alias: FilteredRelation("authors")}) -+ -+ 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 --git a/python-django-5.2.7-CVE-2025-64458.patch b/python-django-5.2.7-CVE-2025-64458.patch deleted file mode 100644 index 959918525573c17221efbb518bbc86cbe4dfeb3d..0000000000000000000000000000000000000000 --- a/python-django-5.2.7-CVE-2025-64458.patch +++ /dev/null @@ -1,65 +0,0 @@ -From 4f5d904b63751dea9ffc3b0e046404a7fa5881ac Mon Sep 17 00:00:00 2001 -From: Jacob Walls -Date: Thu, 16 Oct 2025 16:28:33 -0400 -Subject: [PATCH] [5.2.x] Fixed CVE-2025-64458 -- Mitigated potential DoS in - HttpResponseRedirect/HttpResponsePermanentRedirect on Windows. - -Thanks Seokchan Yoon for the report, Markus Holtermann for the -triage, and Jake Howard for the review. - -Follow-up to CVE-2025-27556 and 39e2297210d9d2938c75fc911d45f0e863dc4821. - -Backport of c880530ddd4fabd5939bab0e148bebe36699432a from main. ---- - django/http/response.py | 9 +++++++-- - tests/httpwrappers/tests.py | 2 ++ - 5 files changed, 37 insertions(+), 4 deletions(-) - -diff --git a/django/http/response.py b/django/http/response.py -index 4a0ea6701375..9f4bd6af9075 100644 ---- a/django/http/response.py -+++ b/django/http/response.py -@@ -22,7 +22,7 @@ - from django.utils.datastructures import CaseInsensitiveMapping - from django.utils.encoding import iri_to_uri - from django.utils.functional import cached_property --from django.utils.http import content_disposition_header, http_date -+from django.utils.http import MAX_URL_LENGTH, content_disposition_header, http_date - from django.utils.regex_helper import _lazy_re_compile - - _charset_from_content_type_re = _lazy_re_compile( -@@ -630,7 +630,12 @@ class HttpResponseRedirectBase(HttpResponse): - def __init__(self, redirect_to, preserve_request=False, *args, **kwargs): - super().__init__(*args, **kwargs) - self["Location"] = iri_to_uri(redirect_to) -- parsed = urlsplit(str(redirect_to)) -+ redirect_to_str = str(redirect_to) -+ if len(redirect_to_str) > MAX_URL_LENGTH: -+ raise DisallowedRedirect( -+ f"Unsafe redirect exceeding {MAX_URL_LENGTH} characters" -+ ) -+ parsed = urlsplit(redirect_to_str) - if preserve_request: - self.status_code = self.status_code_preserve_request - if parsed.scheme and parsed.scheme not in self.allowed_schemes: - -diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py -index f85d33e82338..f62a9d9bba39 100644 ---- a/tests/httpwrappers/tests.py -+++ b/tests/httpwrappers/tests.py -@@ -24,6 +24,7 @@ - ) - from django.test import SimpleTestCase - from django.utils.functional import lazystr -+from django.utils.http import MAX_URL_LENGTH - - - class QueryDictTests(SimpleTestCase): -@@ -490,6 +491,7 @@ def test_unsafe_redirect(self): - 'data:text/html,', - "mailto:test@example.com", - "file:///etc/passwd", -+ "é" * (MAX_URL_LENGTH + 1), - ] - for url in bad_urls: - with self.assertRaises(DisallowedRedirect): diff --git a/python-django-5.2.7-CVE-2025-64459.patch b/python-django-5.2.7-CVE-2025-64459.patch deleted file mode 100644 index 719f7acd1fe3d5793fbb135615ebda2f00f3ba32..0000000000000000000000000000000000000000 --- a/python-django-5.2.7-CVE-2025-64459.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 6703f364d767e949c5b0e4016433ef75063b4f9b Mon Sep 17 00:00:00 2001 -From: Jacob Walls -Date: Wed, 24 Sep 2025 15:54:51 -0400 -Subject: [PATCH] [5.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 +++++ - 5 files changed, 30 insertions(+) - -diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py -index d219c5fb0e1b..9d237cdc7760 100644 ---- a/django/db/models/query_utils.py -+++ b/django/db/models/query_utils.py -@@ -48,8 +48,12 @@ class Q(tree.Node): - XOR = "XOR" - default = AND - conditional = True -+ connectors = (None, AND, OR, XOR) - - 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, - -diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py -index 1a62aca06114..52200b2ecff1 100644 ---- a/tests/queries/test_q.py -+++ b/tests/queries/test_q.py -@@ -272,6 +272,11 @@ def test_create_helper(self): - Q(*items, _connector=connector), - ) - -+ def test_connector_validation(self): -+ msg = f"_connector must be one of {Q.AND!r}, {Q.OR!r}, {Q.XOR!r}, or None." -+ with self.assertRaisesMessage(ValueError, msg): -+ Q(_connector="evil") -+ - def test_referenced_base_fields(self): - # Make sure Q.referenced_base_fields retrieves all base fields from - # both filters and F expressions. diff --git a/python-django-5.2.7-CVE-2025-64460.patch b/python-django-5.2.7-CVE-2025-64460.patch deleted file mode 100644 index 3927fab5fb83ecdae2f4cf68871b398b4308467e..0000000000000000000000000000000000000000 --- a/python-django-5.2.7-CVE-2025-64460.patch +++ /dev/null @@ -1,197 +0,0 @@ -From 99e7d22f55497278d0bcb2e15e72ef532e62a31d Mon Sep 17 00:00:00 2001 -From: Shai Berger -Date: Sat, 11 Oct 2025 21:42:56 +0300 -Subject: [PATCH] [5.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 | 39 +++++++++++++--- - docs/topics/serialization.txt | 2 + - tests/serializers/test_deserialization.py | 54 +++++++++++++++++++++++ - 6 files changed, 119 insertions(+), 6 deletions(-) - -diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py -index 360d5309d853..0fa48acf06e5 100644 ---- a/django/core/serializers/xml_serializer.py -+++ b/django/core/serializers/xml_serializer.py -@@ -3,7 +3,8 @@ - """ - - 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 - -@@ -15,6 +16,25 @@ - from django.utils.xmlutils import SimplerXMLGenerator, UnserializableContentError - - -+@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.""" - -@@ -210,7 +230,8 @@ def _make_parser(self): - 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 - -@@ -394,19 +415,25 @@ def _get_model_from_node(self, node, attr): - - 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 --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt -index 1e573e6e1d53..e9523e2ac133 100644 ---- a/docs/topics/serialization.txt -+++ b/docs/topics/serialization.txt -@@ -173,6 +173,8 @@ Identifier Information - .. _jsonl: https://jsonlines.org/ - .. _PyYAML: https://pyyaml.org/ - -+.. _serialization-formats-xml: -+ - XML - --- - -diff --git a/tests/serializers/test_deserialization.py b/tests/serializers/test_deserialization.py -index 0bbb46b7ce1c..a718a990385a 100644 ---- a/tests/serializers/test_deserialization.py -+++ b/tests/serializers/test_deserialization.py -@@ -1,11 +1,15 @@ - import json -+import time - import unittest - - from django.core.serializers.base import DeserializationError, DeserializedObject - from django.core.serializers.json import Deserializer as JsonDeserializer - from django.core.serializers.jsonl import Deserializer as JsonlDeserializer - from django.core.serializers.python import Deserializer -+from django.core.serializers.xml_serializer import Deserializer as XMLDeserializer -+from django.db import models - from django.test import SimpleTestCase -+from django.test.utils import garbage_collect - - from .models import Author - -@@ -133,3 +137,53 @@ def test_yaml_bytes_input(self): - - self.assertEqual(first_item.object, self.jane) - self.assertEqual(second_item.object, self.joe) -+ -+ def test_crafted_xml_performance(self): -+ """The time to process invalid inputs is not quadratic.""" -+ -+ def build_crafted_xml(depth, leaf_text_len): -+ nested_open = "" * depth -+ nested_close = "" * depth -+ leaf = "x" * leaf_text_len -+ field_content = f"{nested_open}{leaf}{nested_close}" -+ return f""" -+ -+ -+ {field_content} -+ m -+ -+ -+ """ -+ -+ def deserialize(crafted_xml): -+ iterator = XMLDeserializer(crafted_xml) -+ garbage_collect() -+ -+ start_time = time.perf_counter() -+ result = list(iterator) -+ end_time = time.perf_counter() -+ -+ self.assertEqual(len(result), 1) -+ self.assertIsInstance(result[0].object, models.Model) -+ return end_time - start_time -+ -+ def assertFactor(label, params, factor=2): -+ factors = [] -+ prev_time = None -+ for depth, length in params: -+ crafted_xml = build_crafted_xml(depth, length) -+ elapsed = deserialize(crafted_xml) -+ if prev_time is not None: -+ factors.append(elapsed / prev_time) -+ prev_time = elapsed -+ -+ with self.subTest(label): -+ # Assert based on the average factor to reduce test flakiness. -+ self.assertLessEqual(sum(factors) / len(factors), factor) -+ -+ assertFactor( -+ "varying depth, varying length", -+ [(50, 2000), (100, 4000), (200, 8000), (400, 16000), (800, 32000)], -+ 2, -+ ) -+ assertFactor("constant depth, varying length", [(100, 1), (100, 1000)], 2) diff --git a/python-django.spec b/python-django.spec index 143a5ac3dbb83aecc6d125d7581601ba5de20c75..394e4b1f514ffab7b18e1cb4d7e23bbfb6c39141 100644 --- a/python-django.spec +++ b/python-django.spec @@ -3,20 +3,12 @@ Summary: A high-level Python Web framework Name: python-django -Version: 5.2.7 -Release: 3%{?dist} +Version: 5.2.11 +Release: 1%{?dist} License: BSD URL: https://www.djangoproject.com/ Source0: https://github.com/django/django/archive/refs/tags/django-%{version}.tar.gz -%define pyproject_build /usr/bin/python3 -mpip wheel --verbose --progress-bar off --disable-pip-version-check --use-pep517 --no-build-isolation --no-deps --wheel-dir ./pyproject-wheeldir . - -Patch001: python-django-5.2.7-CVE-2025-64458.patch -Patch002: python-django-5.2.7-CVE-2025-64459.patch -Patch003: python-django-5.2.7-CVE-2025-64460.patch -Patch004: python-django-5.2.7-CVE-2025-13372.patch - - BuildArch: noarch @@ -54,6 +46,7 @@ Summary: A high-level Python Web framework BuildRequires: python3-devel python3-setuptools python3-pytz python3-sqlparse python-selenium python3-sphinx BuildRequires: /usr/bin/pathfix.py python3-asgiref BuildRequires: python3-pip python3-wheel +BuildRequires: pyproject-rpm-macros Requires: python3-pytz python3-sqlparse python3-argon2-cffi python3-bcrypt Provides: python-Django, python3-django @@ -70,11 +63,17 @@ without needing to reinvent the wheel. It’s free and open source. pathfix.py -pni "%{__python3} %{py3_shbang_opts}" . %build -%pyproject_build +# Fix license configuration in pyproject.toml to comply with PEP 621 +sed -i 's/^license = "BSD-3-Clause"/license = { text = "BSD-3-Clause" }/' pyproject.toml +# Remove license-files from [project] section as it's not allowed in PEP 621 +sed -i '/^license-files = /d' pyproject.toml + +%pyproject_wheel %install %pyproject_install + pathfix.py -pni "%{__python3} %{py3_shbang_opts}" %{buildroot}/usr/lib/python%{python3_version}/site-packages/django/conf/project_template/manage.py-tpl %if %{with doc} @@ -117,17 +116,22 @@ python3 runtests.py --settings=test_sqlite --verbosity=2 --parallel 1 %endif %files -n python3-django -%license LICENSE -%doc AUTHORS README.rst +%license LICENSE LICENSE.python +%doc AUTHORS README.rst CONTRIBUTING.rst +%{python3_sitelib}/django/ +%{python3_sitelib}/Django-*.dist-info/ %{_bindir}/django-admin %{_bindir}/django-admin-3 %{_bindir}/django-admin-%{python3_version} %{_bindir}/python3-django-admin -%{python3_sitelib}/* %{_mandir}/man1/django-admin.1* %changelog +* Wed Feb 04 2026 Shop You - 5.2.11-1 +- [Type] security +- [DESC] Update to 5.2.11 for fix CVES and license configuration + * Thu Dec 04 2025 Shop You - 5.2.7-3 - [Type] security - [DESC] Fix CVE-2025-64460 and CVE-2025-13372 diff --git a/sources b/sources index 3e6ebbf7ae3f57f127d4990215dde946ac50ba4d..d333eacd1f7f05bccf64e28c1745097d44138ae7 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (django-5.2.7.tar.gz) = df330f665b2e08a27dbe88d60b026158e37dfa722b7896493dade841b91a74a9b38cd7ec9597f101126f618947e35674929cb871fdc4499291eeafb1dbb10946 +SHA512 (django-5.2.11.tar.gz) = 139488bb6c3005d74300e12dfc4a3d5bc241ec501542cf0fbe79539b0df54593e14d83de648de511552bedc5bb5dda1e1303349899970b27ff4a5a29551b8d8a