1
0
forked from pool/python-Django

1 Commits

Author SHA256 Message Date
58adc28d1a CVE-2025-64459 CVE-2025-64460 CVE-2025-13372 2025-12-08 12:08:01 +01:00
5 changed files with 390 additions and 2 deletions

67
CVE-2025-13372.patch Normal file
View File

@@ -0,0 +1,67 @@
From 479415ce5249bcdebeb6570c72df2a87f45a7bbf Mon Sep 17 00:00:00 2001
From: Jacob Walls <jacobtylerwalls@gmail.com>
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 ++++++++++-
docs/releases/4.2.27.txt | 8 ++++++++
docs/releases/5.1.15.txt | 8 ++++++++
docs/releases/5.2.9.txt | 8 ++++++++
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)

108
CVE-2025-64459.patch Normal file
View File

@@ -0,0 +1,108 @@
From 251f22215061abaa5afddd8c8177cf658c8442e8 Mon Sep 17 00:00:00 2001
From: Jacob Walls <jacobtylerwalls@gmail.com>
Date: Wed, 24 Sep 2025 15:54:51 -0400
Subject: [PATCH 2/3] [5.2.x] Fixed CVE-2025-62769 -- 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.
---
django/db/models/query_utils.py | 4 ++++
docs/releases/4.2.26.txt | 7 +++++++
docs/releases/5.1.14.txt | 7 +++++++
docs/releases/5.2.8.txt | 7 +++++++
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 d219c5fb0e..9d237cdc77 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 1a62aca061..52200b2ecf 100644
--- a/tests/queries/test_q.py
+++ b/tests/queries/test_q.py
@@ -272,6 +272,11 @@ class QTests(SimpleTestCase):
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.
--
2.43.0
From b13864f3024abe7afd316081cdf7e67b3b984987 Mon Sep 17 00:00:00 2001
From: Jacob Walls <jacobtylerwalls@gmail.com>
Date: Wed, 24 Sep 2025 15:56:03 -0400
Subject: [PATCH 3/3] [5.2.x] Refs CVE-2025-62769 -- Avoided propagating
invalid arguments to Q on dictionary expansion.
---
django/db/models/query.py | 5 +++++
tests/queries/tests.py | 8 ++++++++
2 files changed, 13 insertions(+)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 535d91d767..8ed028d140 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -42,6 +42,8 @@ MAX_GET_RESULTS = 21
# The maximum number of items to display in a QuerySet.__repr__
REPR_OUTPUT_SIZE = 20
+PROHIBITED_FILTER_KWARGS = frozenset(["_connector", "_negated"])
+
class BaseIterable:
def __init__(
@@ -1512,6 +1514,9 @@ class QuerySet(AltersData):
return clone
def _filter_or_exclude_inplace(self, negate, args, kwargs):
+ if invalid_kwargs := PROHIBITED_FILTER_KWARGS.intersection(kwargs):
+ invalid_kwargs_str = ", ".join(f"'{k}'" for k in sorted(invalid_kwargs))
+ raise TypeError(f"The following kwargs are invalid: {invalid_kwargs_str}")
if negate:
self._query.add_q(~Q(*args, **kwargs))
else:
diff --git a/tests/queries/tests.py b/tests/queries/tests.py
index ffaabf48a0..c1589669b0 100644
--- a/tests/queries/tests.py
+++ b/tests/queries/tests.py
@@ -4506,6 +4506,14 @@ class TestInvalidValuesRelation(SimpleTestCase):
Annotation.objects.filter(tag__in=[123, "abc"])
+class TestInvalidFilterArguments(TestCase):
+ def test_filter_rejects_invalid_arguments(self):
+ school = School.objects.create()
+ msg = "The following kwargs are invalid: '_connector', '_negated'"
+ with self.assertRaisesMessage(TypeError, msg):
+ School.objects.filter(pk=school.pk, _negated=True, _connector="evil")
+
+
class TestTicket24605(TestCase):
def test_ticket_24605(self):
"""
--
2.43.0

199
CVE-2025-64460.patch Normal file
View File

@@ -0,0 +1,199 @@
From 99e7d22f55497278d0bcb2e15e72ef532e62a31d Mon Sep 17 00:00:00 2001
From: Shai Berger <shai@platonix.com>
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 <jacobtylerwalls@gmail.com>
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Backport of 50efb718b31333051bc2dcb06911b8fa1358c98c from main.
---
django/core/serializers/xml_serializer.py | 39 +++++++++++++---
docs/releases/4.2.27.txt | 10 +++++
docs/releases/5.1.15.txt | 10 +++++
docs/releases/5.2.9.txt | 10 +++++
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__ == "<lambda>"
+ 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 = "<nested>" * depth
+ nested_close = "</nested>" * depth
+ leaf = "x" * leaf_text_len
+ field_content = f"{nested_open}{leaf}{nested_close}"
+ return f"""
+ <django-objects version="1.0">
+ <object model="contenttypes.contenttype" pk="1">
+ <field name="app_label">{field_content}</field>
+ <field name="model">m</field>
+ </object>
+ </django-objects>
+ """
+
+ 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)

View File

@@ -1,3 +1,11 @@
-------------------------------------------------------------------
Mon Dec 8 11:04:00 UTC 2025 - Markéta Machová <mmachova@suse.com>
- Add security patches:
* CVE-2025-64459.patch (bsc#1252926)
* CVE-2025-13372.patch (bsc#1254437)
* CVE-2025-64460.patch (bsc#1254437)
-------------------------------------------------------------------
Thu Oct 2 10:01:50 UTC 2025 - Markéta Machová <mmachova@suse.com>

View File

@@ -35,9 +35,15 @@ Patch0: test_strip_tags.patch
# PATCH-FIX-UPSTREAM CVE-2025-57833.patch bsc#1248810
Patch1: CVE-2025-57833.patch
# PATCH-FIX-UPSTREAM CVE-2025-59681.patch bsc#1250485
Patch2: CVE-2025-59681.patch
Patch2: CVE-2025-59681.patch
# PATCH-FIX-UPSTREAM CVE-2025-59682.patch bsc#1250487
Patch3: CVE-2025-59682.patch
Patch3: CVE-2025-59682.patch
# PATCH-FIX-UPSTREAM CVE-2025-64459.patch bsc#1252926
Patch4: CVE-2025-64459.patch
# PATCH-FIX-UPSTREAM CVE-2025-13372.patch bsc#1254437
Patch5: CVE-2025-13372.patch
# PATCH-FIX-UPSTREAM CVE-2025-64460.patch bsc#1254437
Patch6: CVE-2025-64460.patch
BuildRequires: %{python_module Jinja2 >= 2.9.2}
BuildRequires: %{python_module Pillow >= 6.2.0}
BuildRequires: %{python_module PyYAML}