forked from pool/python-Django
Compare commits
1 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| 58adc28d1a |
67
CVE-2025-13372.patch
Normal file
67
CVE-2025-13372.patch
Normal 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
108
CVE-2025-64459.patch
Normal 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
199
CVE-2025-64460.patch
Normal 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)
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user