Compare commits
18 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| 296190542e | |||
| 509b653a93 | |||
| 5b160c9df9 | |||
| d1f5740358 | |||
| a08c10b5c3 | |||
| a1efaf50e9 | |||
| 1cb7e345a7 | |||
| fdcf2d256d | |||
| 17b3c221a5 | |||
| be4f1447ca | |||
| 878b120faf | |||
| 98f379ab77 | |||
| c617be0174 | |||
| 9d51974b18 | |||
| 78db34879c | |||
| b367c16b9f | |||
| 33a429e3af | |||
| c5bef52b95 |
@@ -1,67 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
From 184e38ab0a061c365f5775676a074796d8abd02f Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jake Howard <git@theorangeone.net>
|
|
||||||
Date: Wed, 19 Nov 2025 16:52:28 +0000
|
|
||||||
Subject: [PATCH] [5.2.x] Fixed CVE-2025-13473 -- Standardized timing of
|
|
||||||
check_password() in mod_wsgi auth handler.
|
|
||||||
|
|
||||||
Refs CVE-2024-39329, #20760.
|
|
||||||
|
|
||||||
Thanks Stackered for the report, and Jacob Walls and Markus Holtermann
|
|
||||||
for the reviews.
|
|
||||||
|
|
||||||
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
|
|
||||||
|
|
||||||
Backport of 3eb814e02a4c336866d4189fa0c24fd1875863ed from main.
|
|
||||||
---
|
|
||||||
django/contrib/auth/handlers/modwsgi.py | 37 ++++++++++++++++++++-----
|
|
||||||
docs/releases/4.2.28.txt | 10 +++++++
|
|
||||||
docs/releases/5.2.11.txt | 10 +++++++
|
|
||||||
tests/auth_tests/test_handlers.py | 28 +++++++++++++++++++
|
|
||||||
4 files changed, 78 insertions(+), 7 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py
|
|
||||||
index 591ec72cb4cd..086db89fc846 100644
|
|
||||||
--- a/django/contrib/auth/handlers/modwsgi.py
|
|
||||||
+++ b/django/contrib/auth/handlers/modwsgi.py
|
|
||||||
@@ -4,24 +4,47 @@
|
|
||||||
UserModel = auth.get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
+def _get_user(username):
|
|
||||||
+ """
|
|
||||||
+ Return the UserModel instance for `username`.
|
|
||||||
+
|
|
||||||
+ If no matching user exists, or if the user is inactive, return None, in
|
|
||||||
+ which case the default password hasher is run to mitigate timing attacks.
|
|
||||||
+ """
|
|
||||||
+ try:
|
|
||||||
+ user = UserModel._default_manager.get_by_natural_key(username)
|
|
||||||
+ except UserModel.DoesNotExist:
|
|
||||||
+ user = None
|
|
||||||
+ else:
|
|
||||||
+ if not user.is_active:
|
|
||||||
+ user = None
|
|
||||||
+
|
|
||||||
+ if user is None:
|
|
||||||
+ # Run the default password hasher once to reduce the timing difference
|
|
||||||
+ # between existing/active and nonexistent/inactive users (#20760).
|
|
||||||
+ UserModel().set_password("")
|
|
||||||
+
|
|
||||||
+ return user
|
|
||||||
+
|
|
||||||
+
|
|
||||||
def check_password(environ, username, password):
|
|
||||||
"""
|
|
||||||
Authenticate against Django's auth database.
|
|
||||||
|
|
||||||
mod_wsgi docs specify None, True, False as return value depending
|
|
||||||
on whether the user exists and authenticates.
|
|
||||||
+
|
|
||||||
+ Return None if the user does not exist, return False if the user exists but
|
|
||||||
+ password is not correct, and return True otherwise.
|
|
||||||
+
|
|
||||||
"""
|
|
||||||
# db connection state is managed similarly to the wsgi handler
|
|
||||||
# as mod_wsgi may call these functions outside of a request/response cycle
|
|
||||||
db.reset_queries()
|
|
||||||
try:
|
|
||||||
- try:
|
|
||||||
- user = UserModel._default_manager.get_by_natural_key(username)
|
|
||||||
- except UserModel.DoesNotExist:
|
|
||||||
- return None
|
|
||||||
- if not user.is_active:
|
|
||||||
- return None
|
|
||||||
- return user.check_password(password)
|
|
||||||
+ user = _get_user(username)
|
|
||||||
+ if user:
|
|
||||||
+ return user.check_password(password)
|
|
||||||
finally:
|
|
||||||
db.close_old_connections()
|
|
||||||
|
|
||||||
diff --git a/tests/auth_tests/test_handlers.py b/tests/auth_tests/test_handlers.py
|
|
||||||
index a6b53a9ef11d..32b4371198d4 100644
|
|
||||||
--- a/tests/auth_tests/test_handlers.py
|
|
||||||
+++ b/tests/auth_tests/test_handlers.py
|
|
||||||
@@ -1,4 +1,7 @@
|
|
||||||
+from unittest import mock
|
|
||||||
+
|
|
||||||
from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user
|
|
||||||
+from django.contrib.auth.hashers import get_hasher
|
|
||||||
from django.contrib.auth.models import Group, User
|
|
||||||
from django.test import TransactionTestCase, override_settings
|
|
||||||
|
|
||||||
@@ -73,3 +76,28 @@ def test_groups_for_user(self):
|
|
||||||
|
|
||||||
self.assertEqual(groups_for_user({}, "test"), [b"test_group"])
|
|
||||||
self.assertEqual(groups_for_user({}, "test1"), [])
|
|
||||||
+
|
|
||||||
+ def test_check_password_fake_runtime(self):
|
|
||||||
+ """
|
|
||||||
+ Hasher is run once regardless of whether the user exists. Refs #20760.
|
|
||||||
+ """
|
|
||||||
+ User.objects.create_user("test", "test@example.com", "test")
|
|
||||||
+ User.objects.create_user("inactive", "test@nono.com", "test", is_active=False)
|
|
||||||
+ User.objects.create_user("unusable", "test@nono.com")
|
|
||||||
+
|
|
||||||
+ hasher = get_hasher()
|
|
||||||
+
|
|
||||||
+ for username, password in [
|
|
||||||
+ ("test", "test"),
|
|
||||||
+ ("test", "wrong"),
|
|
||||||
+ ("inactive", "test"),
|
|
||||||
+ ("inactive", "wrong"),
|
|
||||||
+ ("unusable", "test"),
|
|
||||||
+ ("doesnotexist", "test"),
|
|
||||||
+ ]:
|
|
||||||
+ with (
|
|
||||||
+ self.subTest(username=username, password=password),
|
|
||||||
+ mock.patch.object(hasher, "encode") as mock_make_password,
|
|
||||||
+ ):
|
|
||||||
+ check_password({}, username, password)
|
|
||||||
+ mock_make_password.assert_called_once()
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
From 1ba90069c12836db46981bdf75b0e661db5849ce Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jake Howard <git@theorangeone.net>
|
|
||||||
Date: Wed, 14 Jan 2026 15:25:45 +0000
|
|
||||||
Subject: [PATCH] [5.2.x] Fixed CVE-2025-14550 -- Optimized repeated header
|
|
||||||
parsing in ASGI requests.
|
|
||||||
|
|
||||||
Thanks Jiyong Yang for the report, and Natalia Bidart, Jacob Walls, and
|
|
||||||
Shai Berger for reviews.
|
|
||||||
|
|
||||||
Backport of eb22e1d6d643360e952609ef562c139a100ea4eb from main.
|
|
||||||
---
|
|
||||||
django/core/handlers/asgi.py | 7 ++++---
|
|
||||||
docs/releases/4.2.28.txt | 12 ++++++++++++
|
|
||||||
docs/releases/5.2.11.txt | 12 ++++++++++++
|
|
||||||
tests/asgi/tests.py | 28 +++++++++++++++++++++++++++-
|
|
||||||
4 files changed, 55 insertions(+), 4 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py
|
|
||||||
index bb6a6bfb3ce7..2dfcc7f31d3f 100644
|
|
||||||
--- a/django/core/handlers/asgi.py
|
|
||||||
+++ b/django/core/handlers/asgi.py
|
|
||||||
@@ -3,6 +3,7 @@
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import traceback
|
|
||||||
+from collections import defaultdict
|
|
||||||
from contextlib import aclosing
|
|
||||||
|
|
||||||
from asgiref.sync import ThreadSensitiveContext, sync_to_async
|
|
||||||
@@ -83,6 +84,7 @@ def __init__(self, scope, body_file):
|
|
||||||
self.META["SERVER_NAME"] = "unknown"
|
|
||||||
self.META["SERVER_PORT"] = "0"
|
|
||||||
# Headers go into META.
|
|
||||||
+ _headers = defaultdict(list)
|
|
||||||
for name, value in self.scope.get("headers", []):
|
|
||||||
name = name.decode("latin1")
|
|
||||||
if name == "content-length":
|
|
||||||
@@ -94,9 +96,8 @@ def __init__(self, scope, body_file):
|
|
||||||
# HTTP/2 say only ASCII chars are allowed in headers, but decode
|
|
||||||
# latin1 just in case.
|
|
||||||
value = value.decode("latin1")
|
|
||||||
- if corrected_name in self.META:
|
|
||||||
- value = self.META[corrected_name] + "," + value
|
|
||||||
- self.META[corrected_name] = value
|
|
||||||
+ _headers[corrected_name].append(value)
|
|
||||||
+ self.META.update({name: ",".join(value) for name, value in _headers.items()})
|
|
||||||
# Pull out request encoding, if provided.
|
|
||||||
self._set_content_type_params(self.META)
|
|
||||||
# Directly assign the body file to be our stream.
|
|
||||||
diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py
|
|
||||||
index 0b1d3cd60879..81a53e539742 100644
|
|
||||||
--- a/tests/asgi/tests.py
|
|
||||||
+++ b/tests/asgi/tests.py
|
|
||||||
@@ -211,7 +211,7 @@ async def test_post_body(self):
|
|
||||||
self.assertEqual(response_body["type"], "http.response.body")
|
|
||||||
self.assertEqual(response_body["body"], b"Echo!")
|
|
||||||
|
|
||||||
- async def test_create_request_error(self):
|
|
||||||
+ async def test_request_too_big_request_error(self):
|
|
||||||
# Track request_finished signal.
|
|
||||||
signal_handler = SignalHandler()
|
|
||||||
request_finished.connect(signal_handler)
|
|
||||||
@@ -242,6 +242,32 @@ class TestASGIHandler(ASGIHandler):
|
|
||||||
signal_handler.calls[0]["thread"], threading.current_thread()
|
|
||||||
)
|
|
||||||
|
|
||||||
+ async def test_meta_not_modified_with_repeat_headers(self):
|
|
||||||
+ scope = self.async_request_factory._base_scope(path="/", http_version="2.0")
|
|
||||||
+ scope["headers"] = [(b"foo", b"bar")] * 200_000
|
|
||||||
+
|
|
||||||
+ setitem_count = 0
|
|
||||||
+
|
|
||||||
+ class InstrumentedDict(dict):
|
|
||||||
+ def __setitem__(self, *args, **kwargs):
|
|
||||||
+ nonlocal setitem_count
|
|
||||||
+ setitem_count += 1
|
|
||||||
+ super().__setitem__(*args, **kwargs)
|
|
||||||
+
|
|
||||||
+ class InstrumentedASGIRequest(ASGIRequest):
|
|
||||||
+ @property
|
|
||||||
+ def META(self):
|
|
||||||
+ return self._meta
|
|
||||||
+
|
|
||||||
+ @META.setter
|
|
||||||
+ def META(self, value):
|
|
||||||
+ self._meta = InstrumentedDict(**value)
|
|
||||||
+
|
|
||||||
+ request = InstrumentedASGIRequest(scope, None)
|
|
||||||
+
|
|
||||||
+ self.assertEqual(len(request.headers["foo"].split(",")), 200_000)
|
|
||||||
+ self.assertLessEqual(setitem_count, 100)
|
|
||||||
+
|
|
||||||
async def test_cancel_post_request_with_sync_processing(self):
|
|
||||||
"""
|
|
||||||
The request.body object should be available and readable in view
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
From 4c044fcc866ec226f612c475950b690b0139d243 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jake Howard <git@theorangeone.net>
|
|
||||||
Date: Wed, 13 Aug 2025 14:13:42 +0200
|
|
||||||
Subject: [PATCH] [5.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 +
|
|
||||||
docs/releases/4.2.24.txt | 7 +++++++
|
|
||||||
docs/releases/5.1.12.txt | 7 +++++++
|
|
||||||
docs/releases/5.2.6.txt | 7 +++++++
|
|
||||||
tests/annotations/tests.py | 24 ++++++++++++++++++++++++
|
|
||||||
5 files changed, 46 insertions(+)
|
|
||||||
|
|
||||||
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
|
|
||||||
index 9b44d017ffe3..5247616086aa 100644
|
|
||||||
--- a/django/db/models/sql/query.py
|
|
||||||
+++ b/django/db/models/sql/query.py
|
|
||||||
@@ -1696,6 +1696,7 @@ def _add_q(
|
|
||||||
return target_clause, needed_inner
|
|
||||||
|
|
||||||
def add_filtered_relation(self, filtered_relation, alias):
|
|
||||||
+ self.check_alias(alias)
|
|
||||||
filtered_relation.alias = alias
|
|
||||||
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 6c0d7b668c33..060d6324c74c 100644
|
|
||||||
--- a/tests/annotations/tests.py
|
|
||||||
+++ b/tests/annotations/tests.py
|
|
||||||
@@ -14,6 +14,7 @@
|
|
||||||
Exists,
|
|
||||||
ExpressionWrapper,
|
|
||||||
F,
|
|
||||||
+ FilteredRelation,
|
|
||||||
FloatField,
|
|
||||||
Func,
|
|
||||||
IntegerField,
|
|
||||||
@@ -1164,6 +1165,15 @@ def test_alias_sql_injection(self):
|
|
||||||
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.annotate(**{crafted_alias: FilteredRelation("author")})
|
|
||||||
+
|
|
||||||
def test_alias_forbidden_chars(self):
|
|
||||||
tests = [
|
|
||||||
'al"ias',
|
|
||||||
@@ -1189,6 +1199,11 @@ def test_alias_forbidden_chars(self):
|
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
Book.objects.annotate(**{crafted_alias: Value(1)})
|
|
||||||
|
|
||||||
+ with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
+ Book.objects.annotate(
|
|
||||||
+ **{crafted_alias: FilteredRelation("authors")}
|
|
||||||
+ )
|
|
||||||
+
|
|
||||||
@skipUnless(connection.vendor == "postgresql", "PostgreSQL tests")
|
|
||||||
@skipUnlessDBFeature("supports_json_field")
|
|
||||||
def test_set_returning_functions(self):
|
|
||||||
@@ -1482,3 +1497,12 @@ def test_alias_sql_injection(self):
|
|
||||||
)
|
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
Book.objects.alias(**{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")})
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
From b4d3036c04ae71d611edecf5cfc7d4e5b5927f81 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
|
|
||||||
Date: Wed, 10 Sep 2025 09:53:52 +0200
|
|
||||||
Subject: [PATCH 1/2] [5.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.
|
|
||||||
---
|
|
||||||
django/db/models/sql/query.py | 8 ++++----
|
|
||||||
docs/releases/4.2.25.txt | 9 ++++++++-
|
|
||||||
docs/releases/5.1.13.txt | 9 ++++++++-
|
|
||||||
docs/releases/5.2.7.txt | 9 +++++++++
|
|
||||||
tests/aggregation/tests.py | 4 ++--
|
|
||||||
tests/annotations/tests.py | 23 ++++++++++++-----------
|
|
||||||
tests/expressions/test_queryset_values.py | 8 ++++----
|
|
||||||
tests/queries/tests.py | 4 ++--
|
|
||||||
8 files changed, 49 insertions(+), 25 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
|
|
||||||
index 5247616086..3a1cd73951 100644
|
|
||||||
--- a/django/db/models/sql/query.py
|
|
||||||
+++ b/django/db/models/sql/query.py
|
|
||||||
@@ -48,9 +48,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
|
|
||||||
@@ -1208,8 +1208,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, select=True):
|
|
||||||
diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
|
|
||||||
index bf44c4d25f..2e41f19947 100644
|
|
||||||
--- a/tests/aggregation/tests.py
|
|
||||||
+++ b/tests/aggregation/tests.py
|
|
||||||
@@ -2136,8 +2136,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 060d6324c7..7a12121224 100644
|
|
||||||
--- a/tests/annotations/tests.py
|
|
||||||
+++ b/tests/annotations/tests.py
|
|
||||||
@@ -1159,8 +1159,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)})
|
|
||||||
@@ -1168,8 +1168,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")})
|
|
||||||
@@ -1186,13 +1186,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):
|
|
||||||
@@ -1492,8 +1493,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)})
|
|
||||||
@@ -1501,8 +1502,8 @@ class AliasTests(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.alias(**{crafted_alias: FilteredRelation("authors")})
|
|
||||||
diff --git a/tests/expressions/test_queryset_values.py b/tests/expressions/test_queryset_values.py
|
|
||||||
index 47bd1358de..080ee06183 100644
|
|
||||||
--- a/tests/expressions/test_queryset_values.py
|
|
||||||
+++ b/tests/expressions/test_queryset_values.py
|
|
||||||
@@ -37,8 +37,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")})
|
|
||||||
@@ -47,8 +47,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 38b0a5ddfa..ffaabf48a0 100644
|
|
||||||
--- a/tests/queries/tests.py
|
|
||||||
+++ b/tests/queries/tests.py
|
|
||||||
@@ -1961,8 +1961,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"})
|
|
||||||
--
|
|
||||||
2.39.5 (Apple Git-154)
|
|
||||||
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
From 3a7091babcb19f213a25dd5bf8ad90fd63c3cba0 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Sarah Boyce <42296566+sarahboyce@users.noreply.github.com>
|
|
||||||
Date: Tue, 16 Sep 2025 17:13:36 +0200
|
|
||||||
Subject: [PATCH 2/2] [5.2.x] Fixed CVE-2025-59682 -- Fixed potential partial
|
|
||||||
directory-traversal via archive.extract().
|
|
||||||
|
|
||||||
Thanks stackered for the report.
|
|
||||||
|
|
||||||
Follow up to 05413afa8c18cdb978fcdf470e09f7a12b234a23.
|
|
||||||
---
|
|
||||||
django/utils/archive.py | 6 +++++-
|
|
||||||
docs/releases/4.2.25.txt | 8 ++++++++
|
|
||||||
docs/releases/5.1.13.txt | 8 ++++++++
|
|
||||||
docs/releases/5.2.7.txt | 8 ++++++++
|
|
||||||
tests/utils_tests/test_archive.py | 22 ++++++++++++++++++++++
|
|
||||||
5 files changed, 51 insertions(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/django/utils/archive.py b/django/utils/archive.py
|
|
||||||
index 56f34c0038..c05fbcdc97 100644
|
|
||||||
--- a/django/utils/archive.py
|
|
||||||
+++ b/django/utils/archive.py
|
|
||||||
@@ -145,7 +145,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 89a45bc072..24e60039a5 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
|
|
||||||
@@ -94,3 +95,24 @@ 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
|
|
||||||
+
|
|
||||||
+ try:
|
|
||||||
+ tarfile_handle.close()
|
|
||||||
+ 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)
|
|
||||||
+ finally:
|
|
||||||
+ os.remove(tar_path)
|
|
||||||
--
|
|
||||||
2.39.5 (Apple Git-154)
|
|
||||||
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
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,106 +0,0 @@
|
|||||||
From 17a1d64a58ef24c0c3b78d66d86f5415075f18f0 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jacob Walls <jacobtylerwalls@gmail.com>
|
|
||||||
Date: Mon, 19 Jan 2026 15:42:33 -0500
|
|
||||||
Subject: [PATCH] [5.2.x] Fixed CVE-2026-1207 -- Prevented SQL injections in
|
|
||||||
RasterField lookups via band index.
|
|
||||||
|
|
||||||
Thanks Tarek Nakkouch for the report, and Simon Charette for the initial
|
|
||||||
triage and review.
|
|
||||||
|
|
||||||
Backport of 81aa5292967cd09319c45fe2c1a525ce7b6684d8 from main.
|
|
||||||
---
|
|
||||||
.../gis/db/backends/postgis/operations.py | 6 +++
|
|
||||||
docs/releases/4.2.28.txt | 12 +++++
|
|
||||||
docs/releases/5.2.11.txt | 12 +++++
|
|
||||||
tests/gis_tests/rasterapp/test_rasterfield.py | 47 ++++++++++++++++++-
|
|
||||||
4 files changed, 76 insertions(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
|
|
||||||
index 7a347c52878a..c39f756c792e 100644
|
|
||||||
--- a/django/contrib/gis/db/backends/postgis/operations.py
|
|
||||||
+++ b/django/contrib/gis/db/backends/postgis/operations.py
|
|
||||||
@@ -51,6 +51,9 @@ def check_raster(self, lookup, template_params):
|
|
||||||
|
|
||||||
# Look for band indices and inject them if provided.
|
|
||||||
if lookup.band_lhs is not None and lhs_is_raster:
|
|
||||||
+ if not isinstance(lookup.band_lhs, int):
|
|
||||||
+ name = lookup.band_lhs.__class__.__name__
|
|
||||||
+ raise TypeError(f"Band index must be an integer, but got {name!r}.")
|
|
||||||
if not self.func:
|
|
||||||
raise ValueError(
|
|
||||||
"Band indices are not allowed for this operator, it works on bbox "
|
|
||||||
@@ -62,6 +65,9 @@ def check_raster(self, lookup, template_params):
|
|
||||||
)
|
|
||||||
|
|
||||||
if lookup.band_rhs is not None and rhs_is_raster:
|
|
||||||
+ if not isinstance(lookup.band_rhs, int):
|
|
||||||
+ name = lookup.band_rhs.__class__.__name__
|
|
||||||
+ raise TypeError(f"Band index must be an integer, but got {name!r}.")
|
|
||||||
if not self.func:
|
|
||||||
raise ValueError(
|
|
||||||
"Band indices are not allowed for this operator, it works on bbox "
|
|
||||||
diff --git a/tests/gis_tests/rasterapp/test_rasterfield.py b/tests/gis_tests/rasterapp/test_rasterfield.py
|
|
||||||
index 3f2ce770a9a6..89c4ec4856a7 100644
|
|
||||||
--- a/tests/gis_tests/rasterapp/test_rasterfield.py
|
|
||||||
+++ b/tests/gis_tests/rasterapp/test_rasterfield.py
|
|
||||||
@@ -2,7 +2,11 @@
|
|
||||||
|
|
||||||
from django.contrib.gis.db.models.fields import BaseSpatialField
|
|
||||||
from django.contrib.gis.db.models.functions import Distance
|
|
||||||
-from django.contrib.gis.db.models.lookups import DistanceLookupBase, GISLookup
|
|
||||||
+from django.contrib.gis.db.models.lookups import (
|
|
||||||
+ DistanceLookupBase,
|
|
||||||
+ GISLookup,
|
|
||||||
+ RasterBandTransform,
|
|
||||||
+)
|
|
||||||
from django.contrib.gis.gdal import GDALRaster
|
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
|
||||||
from django.contrib.gis.measure import D
|
|
||||||
@@ -356,6 +360,47 @@ def test_lookup_input_band_not_allowed(self):
|
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
qs.count()
|
|
||||||
|
|
||||||
+ def test_lookup_invalid_band_rhs(self):
|
|
||||||
+ rast = GDALRaster(json.loads(JSON_RASTER))
|
|
||||||
+ qs = RasterModel.objects.filter(rast__contains=(rast, "evil"))
|
|
||||||
+ msg = "Band index must be an integer, but got 'str'."
|
|
||||||
+ with self.assertRaisesMessage(TypeError, msg):
|
|
||||||
+ qs.count()
|
|
||||||
+
|
|
||||||
+ def test_lookup_invalid_band_lhs(self):
|
|
||||||
+ """
|
|
||||||
+ Typical left-hand side usage is protected against non-integers, but for
|
|
||||||
+ defense-in-depth purposes, construct custom lookups that evade the
|
|
||||||
+ `int()` and `+ 1` checks in the lookups shipped by django.contrib.gis.
|
|
||||||
+ """
|
|
||||||
+
|
|
||||||
+ # Evade the int() call in RasterField.get_transform().
|
|
||||||
+ class MyRasterBandTransform(RasterBandTransform):
|
|
||||||
+ band_index = "evil"
|
|
||||||
+
|
|
||||||
+ def process_band_indices(self, *args, **kwargs):
|
|
||||||
+ self.band_lhs = self.lhs.band_index
|
|
||||||
+ self.band_rhs, *self.rhs_params = self.rhs_params
|
|
||||||
+
|
|
||||||
+ # Evade the `+ 1` call in BaseSpatialField.process_band_indices().
|
|
||||||
+ ContainsLookup = RasterModel._meta.get_field("rast").get_lookup("contains")
|
|
||||||
+
|
|
||||||
+ class MyContainsLookup(ContainsLookup):
|
|
||||||
+ def process_band_indices(self, *args, **kwargs):
|
|
||||||
+ self.band_lhs = self.lhs.band_index
|
|
||||||
+ self.band_rhs, *self.rhs_params = self.rhs_params
|
|
||||||
+
|
|
||||||
+ RasterField = RasterModel._meta.get_field("rast")
|
|
||||||
+ RasterField.register_lookup(MyContainsLookup, "contains")
|
|
||||||
+ self.addCleanup(RasterField.register_lookup, ContainsLookup, "contains")
|
|
||||||
+
|
|
||||||
+ qs = RasterModel.objects.annotate(
|
|
||||||
+ transformed=MyRasterBandTransform("rast")
|
|
||||||
+ ).filter(transformed__contains=(F("transformed"), 1))
|
|
||||||
+ msg = "Band index must be an integer, but got 'str'."
|
|
||||||
+ with self.assertRaisesMessage(TypeError, msg):
|
|
||||||
+ list(qs)
|
|
||||||
+
|
|
||||||
def test_isvalid_lookup_with_raster_error(self):
|
|
||||||
qs = RasterModel.objects.filter(rast__isvalid=True)
|
|
||||||
msg = (
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
From 9f2ada875bbee62ac46032e38ddb22755d67ae5a Mon Sep 17 00:00:00 2001
|
|
||||||
From: Natalia <124304+nessita@users.noreply.github.com>
|
|
||||||
Date: Wed, 21 Jan 2026 09:53:10 -0300
|
|
||||||
Subject: [PATCH] [5.2.x] Fixed CVE-2026-1285 -- Mitigated potential DoS in
|
|
||||||
django.utils.text.Truncator for HTML input.
|
|
||||||
|
|
||||||
The `TruncateHTMLParser` used `deque.remove()` to remove tags from the
|
|
||||||
stack when processing end tags. With crafted input containing many
|
|
||||||
unmatched end tags, this caused repeated full scans of the tag stack,
|
|
||||||
leading to quadratic time complexity.
|
|
||||||
|
|
||||||
The fix uses LIFO semantics, only removing a tag from the stack when it
|
|
||||||
matches the most recently opened tag. This avoids linear scans for
|
|
||||||
unmatched end tags and reduces complexity to linear time.
|
|
||||||
|
|
||||||
Refs #30686 and 6ee37ada3241ed263d8d1c2901b030d964cbd161.
|
|
||||||
|
|
||||||
Thanks Seokchan Yoon for the report, and Jake Howard and Jacob Walls for
|
|
||||||
reviews.
|
|
||||||
|
|
||||||
Backport of a33540b3e20b5d759aa8b2e4b9ca0e8edd285344 from main.
|
|
||||||
---
|
|
||||||
django/utils/text.py | 9 +++++----
|
|
||||||
docs/releases/4.2.28.txt | 12 ++++++++++++
|
|
||||||
docs/releases/5.2.11.txt | 12 ++++++++++++
|
|
||||||
tests/utils_tests/test_text.py | 10 ++++++++++
|
|
||||||
4 files changed, 39 insertions(+), 4 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/django/utils/text.py b/django/utils/text.py
|
|
||||||
index 26edde99e336..21efb00b98e8 100644
|
|
||||||
--- a/django/utils/text.py
|
|
||||||
+++ b/django/utils/text.py
|
|
||||||
@@ -126,10 +126,11 @@ def handle_starttag(self, tag, attrs):
|
|
||||||
def handle_endtag(self, tag):
|
|
||||||
if tag not in self.void_elements:
|
|
||||||
self.output += f"</{tag}>"
|
|
||||||
- try:
|
|
||||||
- self.tags.remove(tag)
|
|
||||||
- except ValueError:
|
|
||||||
- pass
|
|
||||||
+ # Remove from the stack only if the tag matches the most recently
|
|
||||||
+ # opened tag (LIFO). This avoids O(n) linear scans for unmatched
|
|
||||||
+ # end tags if `deque.remove()` would be called.
|
|
||||||
+ if self.tags and self.tags[0] == tag:
|
|
||||||
+ self.tags.popleft()
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
data, output = self.process(data)
|
|
||||||
diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py
|
|
||||||
index 63c7889cbcec..11c01874cb5d 100644
|
|
||||||
--- a/tests/utils_tests/test_text.py
|
|
||||||
+++ b/tests/utils_tests/test_text.py
|
|
||||||
@@ -202,6 +202,16 @@ def test_truncate_chars_html_with_html_entities(self):
|
|
||||||
truncator = text.Truncator("<p>I <3 python, what about you?</p>")
|
|
||||||
self.assertEqual("<p>I <3 python, wh…</p>", truncator.chars(16, html=True))
|
|
||||||
|
|
||||||
+ def test_truncate_chars_html_with_misnested_tags(self):
|
|
||||||
+ # LIFO removal keeps all tags when a middle tag is closed out of order.
|
|
||||||
+ # With <a><b><c></b>, the </b> doesn't match <c>, so all tags remain
|
|
||||||
+ # in the stack and are properly closed at truncation.
|
|
||||||
+ truncator = text.Truncator("<a><b><c></b>XXXX")
|
|
||||||
+ self.assertEqual(
|
|
||||||
+ truncator.chars(2, html=True, truncate=""),
|
|
||||||
+ "<a><b><c></b>XX</c></b></a>",
|
|
||||||
+ )
|
|
||||||
+
|
|
||||||
def test_truncate_words(self):
|
|
||||||
truncator = text.Truncator("The quick brown fox jumped over the lazy dog.")
|
|
||||||
self.assertEqual(
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
From 3e68ccdc11c127758745ddf0b4954990b14892bc Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jake Howard <git@theorangeone.net>
|
|
||||||
Date: Wed, 21 Jan 2026 11:14:48 +0000
|
|
||||||
Subject: [PATCH] [5.2.x] Fixed CVE-2026-1287 -- Protected against SQL
|
|
||||||
injection in column aliases via control characters.
|
|
||||||
|
|
||||||
Control characters in FilteredRelation column aliases could be used for
|
|
||||||
SQL injection attacks. This affected QuerySet.annotate(), aggregate(),
|
|
||||||
extra(), values(), values_list(), and alias() when using dictionary
|
|
||||||
expansion with **kwargs.
|
|
||||||
|
|
||||||
Thanks Solomon Kebede for the report, and Simon Charette, Jacob Walls,
|
|
||||||
and Natalia Bidart for reviews.
|
|
||||||
|
|
||||||
Backport of e891a84c7ef9962bfcc3b4685690219542f86a22 from main.
|
|
||||||
---
|
|
||||||
django/db/models/sql/query.py | 10 +--
|
|
||||||
docs/releases/4.2.28.txt | 13 ++++
|
|
||||||
docs/releases/5.2.11.txt | 13 ++++
|
|
||||||
tests/aggregation/tests.py | 18 ++++--
|
|
||||||
tests/annotations/tests.py | 74 +++++++++++++++--------
|
|
||||||
tests/expressions/test_queryset_values.py | 36 +++++++----
|
|
||||||
tests/queries/tests.py | 18 ++++--
|
|
||||||
7 files changed, 128 insertions(+), 54 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
|
|
||||||
index 3a1cd739511e..baeac3a05be2 100644
|
|
||||||
--- a/django/db/models/sql/query.py
|
|
||||||
+++ b/django/db/models/sql/query.py
|
|
||||||
@@ -48,9 +48,11 @@
|
|
||||||
|
|
||||||
__all__ = ["Query", "RawQuery"]
|
|
||||||
|
|
||||||
-# Quotation marks ('"`[]), whitespace characters, semicolons, hashes, or inline
|
|
||||||
-# SQL comments are forbidden in column aliases.
|
|
||||||
-FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(r"['`\"\]\[;\s]|#|--|/\*|\*/")
|
|
||||||
+# Quotation marks ('"`[]), whitespace characters, control characters,
|
|
||||||
+# semicolons, hashes, or inline SQL comments are forbidden in column aliases.
|
|
||||||
+FORBIDDEN_ALIAS_PATTERN = _lazy_re_compile(
|
|
||||||
+ r"['`\"\]\[;\s\x00-\x1F\x7F-\x9F]|#|--|/\*|\*/"
|
|
||||||
+)
|
|
||||||
|
|
||||||
# Inspired from
|
|
||||||
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
|
||||||
@@ -1209,7 +1211,7 @@ def check_alias(self, alias):
|
|
||||||
if FORBIDDEN_ALIAS_PATTERN.search(alias):
|
|
||||||
raise ValueError(
|
|
||||||
"Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
- "quotation marks, semicolons, or SQL comments."
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_annotation(self, annotation, alias, select=True):
|
|
||||||
diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py
|
|
||||||
index 2e41f1994723..f621c53e918a 100644
|
|
||||||
--- a/tests/aggregation/tests.py
|
|
||||||
+++ b/tests/aggregation/tests.py
|
|
||||||
@@ -2,6 +2,7 @@
|
|
||||||
import math
|
|
||||||
import re
|
|
||||||
from decimal import Decimal
|
|
||||||
+from itertools import chain
|
|
||||||
|
|
||||||
from django.core.exceptions import FieldError
|
|
||||||
from django.db import connection
|
|
||||||
@@ -2134,13 +2135,18 @@ def test_exists_none_with_aggregate(self):
|
|
||||||
self.assertEqual(len(qs), 6)
|
|
||||||
|
|
||||||
def test_alias_sql_injection(self):
|
|
||||||
- crafted_alias = """injected_name" from "aggregation_author"; --"""
|
|
||||||
msg = (
|
|
||||||
- "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")})
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
+ )
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "aggregation_author"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
+ Author.objects.aggregate(**{crafted_alias: Avg("age")})
|
|
||||||
|
|
||||||
def test_exists_extra_where_with_aggregate(self):
|
|
||||||
qs = Book.objects.annotate(
|
|
||||||
diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
|
|
||||||
index 78e5408d0fe7..0430c68f9175 100644
|
|
||||||
--- a/tests/annotations/tests.py
|
|
||||||
+++ b/tests/annotations/tests.py
|
|
||||||
@@ -1,5 +1,6 @@
|
|
||||||
import datetime
|
|
||||||
from decimal import Decimal
|
|
||||||
+from itertools import chain
|
|
||||||
from unittest import skipUnless
|
|
||||||
|
|
||||||
from django.core.exceptions import FieldDoesNotExist, FieldError
|
|
||||||
@@ -1157,22 +1158,32 @@ def test_annotation_aggregate_with_m2o(self):
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_alias_sql_injection(self):
|
|
||||||
- crafted_alias = """injected_name" from "annotations_book"; --"""
|
|
||||||
msg = (
|
|
||||||
- "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)})
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
+ )
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "annotations_book"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ 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, hashes, quotation "
|
|
||||||
- "marks, semicolons, or SQL comments."
|
|
||||||
- )
|
|
||||||
- with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
- Book.objects.annotate(**{crafted_alias: FilteredRelation("author")})
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
+ )
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "annotations_book"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
+ Book.objects.annotate(**{crafted_alias: FilteredRelation("author")})
|
|
||||||
|
|
||||||
def test_alias_forbidden_chars(self):
|
|
||||||
tests = [
|
|
||||||
@@ -1190,10 +1201,11 @@ def test_alias_forbidden_chars(self):
|
|
||||||
"alias[",
|
|
||||||
"alias]",
|
|
||||||
"ali#as",
|
|
||||||
+ "ali\0as",
|
|
||||||
]
|
|
||||||
msg = (
|
|
||||||
- "Column aliases cannot contain whitespace characters, hashes, quotation "
|
|
||||||
- "marks, semicolons, or SQL comments."
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
)
|
|
||||||
for crafted_alias in tests:
|
|
||||||
with self.subTest(crafted_alias):
|
|
||||||
@@ -1491,22 +1503,32 @@ def test_alias_after_values(self):
|
|
||||||
self.assertEqual(qs.get(pk=self.b1.pk), (self.b1.pk,))
|
|
||||||
|
|
||||||
def test_alias_sql_injection(self):
|
|
||||||
- crafted_alias = """injected_name" from "annotations_book"; --"""
|
|
||||||
msg = (
|
|
||||||
- "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)})
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
+ )
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "annotations_book"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
+ Book.objects.alias(**{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, hashes, quotation "
|
|
||||||
- "marks, semicolons, or SQL comments."
|
|
||||||
- )
|
|
||||||
- with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
- Book.objects.alias(**{crafted_alias: FilteredRelation("authors")})
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
+ )
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "annotations_book"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ 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(
|
|
||||||
diff --git a/tests/expressions/test_queryset_values.py b/tests/expressions/test_queryset_values.py
|
|
||||||
index 080ee06183dc..afd8a51159d5 100644
|
|
||||||
--- a/tests/expressions/test_queryset_values.py
|
|
||||||
+++ b/tests/expressions/test_queryset_values.py
|
|
||||||
@@ -1,3 +1,5 @@
|
|
||||||
+from itertools import chain
|
|
||||||
+
|
|
||||||
from django.db.models import F, Sum
|
|
||||||
from django.test import TestCase, skipUnlessDBFeature
|
|
||||||
|
|
||||||
@@ -35,26 +37,36 @@ def test_values_expression(self):
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_values_expression_alias_sql_injection(self):
|
|
||||||
- crafted_alias = """injected_name" from "expressions_company"; --"""
|
|
||||||
msg = (
|
|
||||||
- "Column aliases cannot contain whitespace characters, hashes, quotation "
|
|
||||||
- "marks, semicolons, or SQL comments."
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
)
|
|
||||||
- with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
- Company.objects.values(**{crafted_alias: F("ceo__salary")})
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "expressions_company"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ 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, hashes, quotation "
|
|
||||||
- "marks, semicolons, or SQL comments."
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
)
|
|
||||||
- with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
- JSONFieldModel.objects.values(f"data__{crafted_alias}")
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "expressions_company"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(chr(c) for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ 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}")
|
|
||||||
+ 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
|
|
||||||
diff --git a/tests/queries/tests.py b/tests/queries/tests.py
|
|
||||||
index c1589669b073..69d728b1cd36 100644
|
|
||||||
--- a/tests/queries/tests.py
|
|
||||||
+++ b/tests/queries/tests.py
|
|
||||||
@@ -2,6 +2,7 @@
|
|
||||||
import pickle
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
+from itertools import chain
|
|
||||||
from operator import attrgetter
|
|
||||||
|
|
||||||
from django.core.exceptions import EmptyResultSet, FieldError, FullResultSet
|
|
||||||
@@ -1959,13 +1960,18 @@ def test_extra_select_literal_percent_s(self):
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_extra_select_alias_sql_injection(self):
|
|
||||||
- crafted_alias = """injected_name" from "queries_note"; --"""
|
|
||||||
msg = (
|
|
||||||
- "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"})
|
|
||||||
+ "Column aliases cannot contain whitespace characters, hashes, "
|
|
||||||
+ "control characters, quotation marks, semicolons, or SQL comments."
|
|
||||||
+ )
|
|
||||||
+ for crafted_alias in [
|
|
||||||
+ """injected_name" from "queries_note"; --""",
|
|
||||||
+ # Control characters.
|
|
||||||
+ *(f"name{chr(c)}" for c in chain(range(32), range(0x7F, 0xA0))),
|
|
||||||
+ ]:
|
|
||||||
+ with self.subTest(crafted_alias):
|
|
||||||
+ with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
+ Note.objects.extra(select={crafted_alias: "1"})
|
|
||||||
|
|
||||||
def test_queryset_reuse(self):
|
|
||||||
# Using querysets doesn't mutate aliases.
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
From ab0ad8d39555292b55123adeac57ed64c776f8d9 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jacob Walls <jacobtylerwalls@gmail.com>
|
|
||||||
Date: Wed, 21 Jan 2026 18:00:13 -0500
|
|
||||||
Subject: [PATCH] [5.2.x] Refs CVE-2026-1312 -- Raised ValueError when
|
|
||||||
FilteredRelation aliases contain periods.
|
|
||||||
|
|
||||||
This prevents failures at the database layer, given that aliases in the
|
|
||||||
ON clause are not quoted.
|
|
||||||
|
|
||||||
Systematically quoting aliases even in FilteredRelation is tracked in
|
|
||||||
https://code.djangoproject.com/ticket/36795.
|
|
||||||
|
|
||||||
Backport of 005d60d97c4dfb117503bdb6f2facfcaf9315d84 from main.
|
|
||||||
---
|
|
||||||
django/db/models/sql/query.py | 5 +++++
|
|
||||||
tests/filtered_relation/tests.py | 13 +++++++++++++
|
|
||||||
tests/ordering/tests.py | 11 ++++++++---
|
|
||||||
3 files changed, 26 insertions(+), 3 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
|
|
||||||
index baeac3a05be2..324e605b8eea 100644
|
|
||||||
--- a/django/db/models/sql/query.py
|
|
||||||
+++ b/django/db/models/sql/query.py
|
|
||||||
@@ -1698,6 +1698,11 @@ def _add_q(
|
|
||||||
return target_clause, needed_inner
|
|
||||||
|
|
||||||
def add_filtered_relation(self, filtered_relation, alias):
|
|
||||||
+ if "." in alias:
|
|
||||||
+ raise ValueError(
|
|
||||||
+ "FilteredRelation doesn't support aliases with periods "
|
|
||||||
+ "(got %r)." % alias
|
|
||||||
+ )
|
|
||||||
self.check_alias(alias)
|
|
||||||
filtered_relation.alias = alias
|
|
||||||
relation_lookup_parts, relation_field_parts, _ = self.solve_lookup_type(
|
|
||||||
diff --git a/tests/filtered_relation/tests.py b/tests/filtered_relation/tests.py
|
|
||||||
index a9c6e00567cf..a4561d59f95f 100644
|
|
||||||
--- a/tests/filtered_relation/tests.py
|
|
||||||
+++ b/tests/filtered_relation/tests.py
|
|
||||||
@@ -216,6 +216,19 @@ def test_internal_queryset_alias_mapping(self):
|
|
||||||
str(queryset.query),
|
|
||||||
)
|
|
||||||
|
|
||||||
+ def test_period_forbidden(self):
|
|
||||||
+ msg = (
|
|
||||||
+ "FilteredRelation doesn't support aliases with periods (got 'book.alice')."
|
|
||||||
+ )
|
|
||||||
+ with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
+ Author.objects.annotate(
|
|
||||||
+ **{
|
|
||||||
+ "book.alice": FilteredRelation(
|
|
||||||
+ "book", condition=Q(book__title__iexact="poem by alice")
|
|
||||||
+ )
|
|
||||||
+ }
|
|
||||||
+ )
|
|
||||||
+
|
|
||||||
def test_multiple(self):
|
|
||||||
qs = (
|
|
||||||
Author.objects.annotate(
|
|
||||||
diff --git a/tests/ordering/tests.py b/tests/ordering/tests.py
|
|
||||||
index 530a27920e9d..30da025875bd 100644
|
|
||||||
--- a/tests/ordering/tests.py
|
|
||||||
+++ b/tests/ordering/tests.py
|
|
||||||
@@ -15,7 +15,6 @@
|
|
||||||
Value,
|
|
||||||
)
|
|
||||||
from django.db.models.functions import Length, Upper
|
|
||||||
-from django.db.utils import DatabaseError
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
@@ -408,13 +407,19 @@ def test_alias_with_period_shadows_table_name(self):
|
|
||||||
self.assertNotEqual(qs[0].headline, "Backdated")
|
|
||||||
|
|
||||||
relation = FilteredRelation("author")
|
|
||||||
- qs2 = Article.objects.annotate(**{crafted: relation}).order_by(crafted)
|
|
||||||
- with self.assertRaises(DatabaseError):
|
|
||||||
+ msg = (
|
|
||||||
+ "FilteredRelation doesn't support aliases with periods "
|
|
||||||
+ "(got 'ordering_article.pub_date')."
|
|
||||||
+ )
|
|
||||||
+ with self.assertRaisesMessage(ValueError, msg):
|
|
||||||
+ qs2 = Article.objects.annotate(**{crafted: relation}).order_by(crafted)
|
|
||||||
# Before, unlike F(), which causes ordering expressions to be
|
|
||||||
# replaced by ordinals like n in ORDER BY n, these were ordered by
|
|
||||||
# pub_date instead of author.
|
|
||||||
# The Article model orders by -pk, so sorting on author will place
|
|
||||||
# first any article by author2 instead of the backdated one.
|
|
||||||
+ # This assertion is reachable if FilteredRelation.__init__() starts
|
|
||||||
+ # supporting periods in aliases in the future.
|
|
||||||
self.assertNotEqual(qs2[0].headline, "Backdated")
|
|
||||||
|
|
||||||
def test_order_by_pk(self):
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
From e863ee273c6553e9b6fa4960a17acb535851857b Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jacob Walls <jacobtylerwalls@gmail.com>
|
|
||||||
Date: Wed, 21 Jan 2026 17:53:52 -0500
|
|
||||||
Subject: [PATCH] [5.2.x] Fixed CVE-2026-1312 -- Protected order_by() from SQL
|
|
||||||
injection via aliases with periods.
|
|
||||||
|
|
||||||
Before, `order_by()` treated a period in a field name as a sign that it
|
|
||||||
was requested via `.extra(order_by=...)` and thus should be passed
|
|
||||||
through as raw table and column names, even if `extra()` was not used.
|
|
||||||
Since periods are permitted in aliases, this meant user-controlled
|
|
||||||
aliases could force the `order_by()` clause to resolve to a raw table
|
|
||||||
and column pair instead of the actual target field for the alias.
|
|
||||||
|
|
||||||
In practice, only `FilteredRelation` was affected, as the other
|
|
||||||
expressions we tested, e.g. `F`, aggressively optimize away the ordering
|
|
||||||
expressions into ordinal positions, e.g. ORDER BY 2, instead of ORDER BY
|
|
||||||
"table".column.
|
|
||||||
|
|
||||||
Thanks Solomon Kebede for the report, and Simon Charette and Jake Howard
|
|
||||||
for reviews.
|
|
||||||
|
|
||||||
Backport of 69065ca869b0970dff8fdd8fafb390bf8b3bf222 from main.
|
|
||||||
---
|
|
||||||
django/db/models/sql/compiler.py | 2 +-
|
|
||||||
docs/releases/4.2.28.txt | 10 ++++++++++
|
|
||||||
docs/releases/5.2.11.txt | 10 ++++++++++
|
|
||||||
tests/ordering/tests.py | 25 +++++++++++++++++++++++++
|
|
||||||
4 files changed, 46 insertions(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
|
|
||||||
index 4292243c71ee..7dbdec363568 100644
|
|
||||||
--- a/django/db/models/sql/compiler.py
|
|
||||||
+++ b/django/db/models/sql/compiler.py
|
|
||||||
@@ -433,7 +433,7 @@ def _order_by_pairs(self):
|
|
||||||
yield OrderBy(expr, descending=descending), False
|
|
||||||
continue
|
|
||||||
|
|
||||||
- if "." in field:
|
|
||||||
+ if "." in field and field in self.query.extra_order_by:
|
|
||||||
# This came in through an extra(order_by=...) addition. Pass it
|
|
||||||
# on verbatim.
|
|
||||||
table, col = col.split(".", 1)
|
|
||||||
diff --git a/tests/ordering/tests.py b/tests/ordering/tests.py
|
|
||||||
index b29404ed77da..530a27920e9d 100644
|
|
||||||
--- a/tests/ordering/tests.py
|
|
||||||
+++ b/tests/ordering/tests.py
|
|
||||||
@@ -7,6 +7,7 @@
|
|
||||||
Count,
|
|
||||||
DateTimeField,
|
|
||||||
F,
|
|
||||||
+ FilteredRelation,
|
|
||||||
Max,
|
|
||||||
OrderBy,
|
|
||||||
OuterRef,
|
|
||||||
@@ -14,6 +15,7 @@
|
|
||||||
Value,
|
|
||||||
)
|
|
||||||
from django.db.models.functions import Length, Upper
|
|
||||||
+from django.db.utils import DatabaseError
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from .models import (
|
|
||||||
@@ -392,6 +394,29 @@ def test_extra_ordering_with_table_name(self):
|
|
||||||
attrgetter("headline"),
|
|
||||||
)
|
|
||||||
|
|
||||||
+ def test_alias_with_period_shadows_table_name(self):
|
|
||||||
+ """
|
|
||||||
+ Aliases with periods are not confused for table names from extra().
|
|
||||||
+ """
|
|
||||||
+ Article.objects.update(author=self.author_2)
|
|
||||||
+ Article.objects.create(
|
|
||||||
+ headline="Backdated", pub_date=datetime(1900, 1, 1), author=self.author_1
|
|
||||||
+ )
|
|
||||||
+ crafted = "ordering_article.pub_date"
|
|
||||||
+
|
|
||||||
+ qs = Article.objects.annotate(**{crafted: F("author")}).order_by("-" + crafted)
|
|
||||||
+ self.assertNotEqual(qs[0].headline, "Backdated")
|
|
||||||
+
|
|
||||||
+ relation = FilteredRelation("author")
|
|
||||||
+ qs2 = Article.objects.annotate(**{crafted: relation}).order_by(crafted)
|
|
||||||
+ with self.assertRaises(DatabaseError):
|
|
||||||
+ # Before, unlike F(), which causes ordering expressions to be
|
|
||||||
+ # replaced by ordinals like n in ORDER BY n, these were ordered by
|
|
||||||
+ # pub_date instead of author.
|
|
||||||
+ # The Article model orders by -pk, so sorting on author will place
|
|
||||||
+ # first any article by author2 instead of the backdated one.
|
|
||||||
+ self.assertNotEqual(qs2[0].headline, "Backdated")
|
|
||||||
+
|
|
||||||
def test_order_by_pk(self):
|
|
||||||
"""
|
|
||||||
'pk' works as an ordering option in Meta.
|
|
||||||
67
Django-5.2.10.checksum.txt
Normal file
67
Django-5.2.10.checksum.txt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
-----BEGIN PGP SIGNED MESSAGE-----
|
||||||
|
Hash: SHA256
|
||||||
|
|
||||||
|
This file contains MD5, SHA1, and SHA256 checksums for the
|
||||||
|
source-code tarball and wheel files of Django 5.2.10, released January 6, 2026.
|
||||||
|
|
||||||
|
To use this file, you will need a working install of PGP or other
|
||||||
|
compatible public-key encryption software. You will also need to have
|
||||||
|
the Django release manager's public key in your keyring. This key has
|
||||||
|
the ID ``131403F4D16D8DC7`` and can be imported from the MIT
|
||||||
|
keyserver, for example, if using the open-source GNU Privacy Guard
|
||||||
|
implementation of PGP:
|
||||||
|
|
||||||
|
gpg --keyserver pgp.mit.edu --recv-key 131403F4D16D8DC7
|
||||||
|
|
||||||
|
or via the GitHub API:
|
||||||
|
|
||||||
|
curl https://github.com/jacobtylerwalls.gpg | gpg --import -
|
||||||
|
|
||||||
|
Once the key is imported, verify this file:
|
||||||
|
|
||||||
|
gpg --verify Django-5.2.10.checksum.txt
|
||||||
|
|
||||||
|
Once you have verified this file, you can use normal MD5, SHA1, or SHA256
|
||||||
|
checksumming applications to generate the checksums of the Django
|
||||||
|
package and compare them to the checksums listed below.
|
||||||
|
|
||||||
|
Release packages
|
||||||
|
================
|
||||||
|
|
||||||
|
https://www.djangoproject.com/download/5.2.10/tarball/
|
||||||
|
https://www.djangoproject.com/download/5.2.10/wheel/
|
||||||
|
|
||||||
|
MD5 checksums
|
||||||
|
=============
|
||||||
|
|
||||||
|
98e667c17123f7bbd2d7d1db32f9ccdd django-5.2.10.tar.gz
|
||||||
|
18c82b021ddfeae5703023a784e46945 django-5.2.10-py3-none-any.whl
|
||||||
|
|
||||||
|
SHA1 checksums
|
||||||
|
==============
|
||||||
|
|
||||||
|
a215351eb827f0f8dbab6a83db334dfd45a040d8 django-5.2.10.tar.gz
|
||||||
|
363f7332c354a91cfd40cbcd4a1df4291064fc64 django-5.2.10-py3-none-any.whl
|
||||||
|
|
||||||
|
SHA256 checksums
|
||||||
|
================
|
||||||
|
|
||||||
|
74df100784c288c50a2b5cad59631d71214f40f72051d5af3fdf220c20bdbbbe django-5.2.10.tar.gz
|
||||||
|
cf85067a64250c95d5f9067b056c5eaa80591929f7e16fbcd997746e40d6c45c django-5.2.10-py3-none-any.whl
|
||||||
|
|
||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAEBCAAdFiEEU9RpQuAGoqPu3IvIExQD9NFtjccFAmldU1gACgkQExQD9NFt
|
||||||
|
jccJlA//cN08kKwniqRtHV7cxkqugTTP11AiZirz/QDjWnzmyTT9cUEv7sbHF3An
|
||||||
|
uJQyVaqHuOITbi437vuLyBR4Z+tN1XA3AGe+TdXeHWmmHhDS2xMzuDUuBzrACfVp
|
||||||
|
+H1pFSCVtp0koLJHUOtOHmxAsjsBirCJDawY8x2t5cpAiHH1bUn/NkpcNbSKJnUV
|
||||||
|
FhdbygZnV9ZLbnhsyu++Ym7XT3QdoL4V2GsYsqru3hlwei3jexVZx9RjIUC/krqs
|
||||||
|
hgC3NyOs0KvHyehjAKZdvcIjFa38mpMxmpXdWpMMPyoN0p6SdBQ8cE+oa83jsrzK
|
||||||
|
YeHQgGXhbB3ALJ4mIwJirIyyMmEQdTx/Zeh++IayGUUzuoNWi88b3IGCNOOhGRPp
|
||||||
|
tr0EqM4YKr1pgmNibr5UGwHk1bW3ugT2nUEIs2XnQUb3ItQMEyeh2scuL2IpSkVW
|
||||||
|
EWJb7CT3WomxSc8xWRm39/g9voXBhQt3dtUBv/VL+MdBBqsK5fL14A0tKd8HagBJ
|
||||||
|
jo1xVnO6730CIsM61ydHhi5ZGCoXADBS1IFPYPGeaTN3xsMsRPjEkf8KFI/idmkE
|
||||||
|
HnvuZq3Z0h6ipTR+zdzzNhpP+6VOKBaxgDeU64DGYlSEMvdFJE8HQhmzz9Pks0va
|
||||||
|
k6qCiXOskgozRGFuiFj2KUQ+a4Qj3UwSOgg6Z5ztbU2aTytgG38=
|
||||||
|
=GJeZ
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNED MESSAGE-----
|
|
||||||
Hash: SHA256
|
|
||||||
|
|
||||||
This file contains MD5, SHA1, and SHA256 checksums for the
|
|
||||||
source-code tarball and wheel files of Django 5.2.4, released July 2, 2025.
|
|
||||||
|
|
||||||
To use this file, you will need a working install of PGP or other
|
|
||||||
compatible public-key encryption software. You will also need to have
|
|
||||||
the Django release manager's public key in your keyring. This key has
|
|
||||||
the ID ``2EE82A8D9470983E`` and can be imported from the MIT
|
|
||||||
keyserver, for example, if using the open-source GNU Privacy Guard
|
|
||||||
implementation of PGP:
|
|
||||||
|
|
||||||
gpg --keyserver pgp.mit.edu --recv-key 2EE82A8D9470983E
|
|
||||||
|
|
||||||
or via the GitHub API:
|
|
||||||
|
|
||||||
curl https://github.com/nessita.gpg | gpg --import -
|
|
||||||
|
|
||||||
Once the key is imported, verify this file:
|
|
||||||
|
|
||||||
gpg --verify Django-5.2.4.checksum.txt
|
|
||||||
|
|
||||||
Once you have verified this file, you can use normal MD5, SHA1, or SHA256
|
|
||||||
checksumming applications to generate the checksums of the Django
|
|
||||||
package and compare them to the checksums listed below.
|
|
||||||
|
|
||||||
Release packages
|
|
||||||
================
|
|
||||||
|
|
||||||
https://www.djangoproject.com/download/5.2.4/tarball/
|
|
||||||
https://www.djangoproject.com/download/5.2.4/wheel/
|
|
||||||
|
|
||||||
MD5 checksums
|
|
||||||
=============
|
|
||||||
|
|
||||||
6ecc4875e8cdc08706faea1cc4740fdf django-5.2.4.tar.gz
|
|
||||||
fee657f7686462d388f274c5f92b634a django-5.2.4-py3-none-any.whl
|
|
||||||
|
|
||||||
SHA1 checksums
|
|
||||||
==============
|
|
||||||
|
|
||||||
de45d44e1bb2ceb1c08b8fd0846de920874f71a1 django-5.2.4.tar.gz
|
|
||||||
a6a7904e3749a0e8937a50643293889929b4b6f7 django-5.2.4-py3-none-any.whl
|
|
||||||
|
|
||||||
SHA256 checksums
|
|
||||||
================
|
|
||||||
|
|
||||||
a1228c384f8fa13eebc015196db7b3e08722c5058d4758d20cb287503a540d8f django-5.2.4.tar.gz
|
|
||||||
60c35bd96201b10c6e7a78121bd0da51084733efa303cc19ead021ab179cef5e django-5.2.4-py3-none-any.whl
|
|
||||||
|
|
||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQJcBAEBCABGFiEEW1sboQ2FrHxcduOPLugqjZRwmD4FAmhlfcIoHDEyNDMwNCtu
|
|
||||||
ZXNzaXRhQHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbQAKCRAu6CqNlHCYPj5DD/94
|
|
||||||
KOuOZ5JHtZWknqi1JeV1akzB/RpY7lhL9SbJbVXhdAxOY9Cn4eUG7NsPWa9JnhX1
|
|
||||||
F/2geBE5mjOZen4ARtGHWxa5vqidqUbscrU9AkqPLn6aecEKi2jXXNkYmmWw/37K
|
|
||||||
wb92BQtuWkaXyiZ4E6Sledx9yFhcqMDFg27CdNYfAqUWofI6zzSmLIzOlOSVR9Sc
|
|
||||||
uDrfRqQ4GXlRGT5pIkcIxE0ZKToUYrKgn99PZOmBcLfJgQ4VBt62J6SzZAhhElb3
|
|
||||||
DUMcVhG2XNIhg7v7DwlVodowDYQdRi2H/ahAa7/m1+uugRbysoGSLLwP+50tDjlj
|
|
||||||
07zxoJsrL5R9zaMp4pcXQN4bUy3rDz94DkjlXO51f8LwDdStvk4VOYan1W5S9BhP
|
|
||||||
R0conCFfcg4+iK0pV5e/GeeTwBRHQw8p5RuWfrEpKFi/XQtT0u01hqUGppeuZ9wI
|
|
||||||
f+Ud9RA8Nrw0ouli4WvfH0RVFuMgUFqScwO88oatuUH5CDPjlV+5usNb7FrmZXv6
|
|
||||||
AWRopONOcYGF07+FYh0nsoE8enWyxE+JWTJzxT5PGZ3buUO0hlnJ+auoJv8yOVii
|
|
||||||
ELCSUyi93glWonCBrS41XrNO6+6K/8V9V6iv9/PdGwF1GszbX5Rx4e2lDMA7crYh
|
|
||||||
1qKGaV3+iAO+Y+vXt6VTy6h5GLg9hun+RQ8TU3Guyg==
|
|
||||||
=d9C5
|
|
||||||
-----END PGP SIGNATURE-----
|
|
||||||
3
django-5.2.10.tar.gz
Normal file
3
django-5.2.10.tar.gz
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:74df100784c288c50a2b5cad59631d71214f40f72051d5af3fdf220c20bdbbbe
|
||||||
|
size 10880754
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:a1228c384f8fa13eebc015196db7b3e08722c5058d4758d20cb287503a540d8f
|
|
||||||
size 10831909
|
|
||||||
@@ -1,41 +1,89 @@
|
|||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Wed Feb 4 13:21:44 UTC 2026 - Markéta Machová <mmachova@suse.com>
|
Fri Jan 9 09:41:44 UTC 2026 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|
||||||
|
- Update to 5.2.10
|
||||||
|
* Fixed a bug in Django 5.2 where data exceeding max_length was
|
||||||
|
silently truncated by QuerySet.bulk_create() on PostgreSQL
|
||||||
|
* Fixed a bug where management command colorized help (introduced
|
||||||
|
in Python 3.14) ignored the --no-color option and the DJANGO_COLORS
|
||||||
|
setting
|
||||||
|
- Drop merged test_strip_tags_incomplete.patch
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Mon Dec 29 09:10:21 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|
||||||
- Add test_strip_tags_incomplete.patch to fix behaviour with changes
|
- Add test_strip_tags_incomplete.patch to fix behaviour with changes
|
||||||
in the Python interpreter
|
in the Python interpreter
|
||||||
- Rebase test_strip_tags.patch
|
- Rebase test_strip_tags.patch
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Wed Feb 4 11:00:19 UTC 2026 - Markéta Machová <mmachova@suse.com>
|
Mon Dec 8 11:54:46 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|
||||||
- Add security patches:
|
- Update to 5.2.9 (bsc#1254437)
|
||||||
* CVE-2025-14550.patch (bsc#1257403)
|
* CVE-2025-13372: Potential SQL injection in FilteredRelation column
|
||||||
* CVE-2026-1312.patch (bsc#1257408)
|
aliases on PostgreSQL
|
||||||
* CVE-2026-1312-followup.patch (bsc#1257408)
|
* CVE-2025-64460: Potential denial-of-service vulnerability in XML
|
||||||
* CVE-2026-1287.patch (bsc#1257407)
|
Deserializer
|
||||||
* CVE-2026-1207.patch (bsc#1257405)
|
* Fixed a crash on Python 3.14+ that prevented template tag functions
|
||||||
* CVE-2025-13473.patch (bsc#1257401)
|
from being registered
|
||||||
* CVE-2026-1285.patch (bsc#1257406)
|
* Fixed more bugs and regressions, see upstream release notes
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Mon Dec 8 11:04:00 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
Thu Nov 6 07:20:08 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|
||||||
- Add security patches:
|
- Update to 5.2.8 (bsc#1252926)
|
||||||
* CVE-2025-64459.patch (bsc#1252926)
|
* CVE-2025-64459: Potential SQL injection via _connector keyword argument
|
||||||
* CVE-2025-13372.patch (bsc#1254437)
|
* Added compatibility for oracledb 3.4.0
|
||||||
* CVE-2025-64460.patch (bsc#1254437)
|
* Fixed a bug in Django 5.2 where QuerySet.first() and QuerySet.last()
|
||||||
|
raised an error on querysets performing aggregation that selected all
|
||||||
|
fields of a composite primary key.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Thu Oct 2 10:01:50 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
Thu Oct 2 09:41:30 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|
||||||
- Add security patches:
|
- Update to 5.2.7 (bsc#1250485, bsc#1250487)
|
||||||
* CVE-2025-59681.patch (bsc#1250485)
|
* CVE-2025-59681: Potential SQL injection in QuerySet.annotate(), alias(),
|
||||||
* CVE-2025-59682.patch (bsc#1250487)
|
aggregate(), and extra() on MySQL and MariaDB
|
||||||
|
* CVE-2025-59682: Potential partial directory-traversal via archive.extract()
|
||||||
|
* Fixed a regression in Django 5.2 that reduced the color contrast of the
|
||||||
|
label of filter_horizontal and filter_vertical widgets within a TabularInline
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Thu Sep 4 13:14:40 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
Thu Sep 4 10:02:00 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|
||||||
- Add security patch CVE-2025-57833.patch (bsc#1248810)
|
- Update to 5.2.6 (bsc#1248810)
|
||||||
|
* CVE-2025-57833: Potential SQL injection in FilteredRelation column aliases
|
||||||
|
* Fixed a bug where using QuerySet.values() or values_list() with a ForeignObject
|
||||||
|
composed of multiple fields returned incorrect results instead of tuples of
|
||||||
|
the referenced fields
|
||||||
|
- Rebased test_strip_tags.patch
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Tue Aug 12 10:52:26 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|
||||||
|
- Update to 5.2.5
|
||||||
|
* Fixed a regression in Django 5.2.1 that prevented the usage of UNNEST
|
||||||
|
PostgreSQL strategy of QuerySet.bulk_create() with foreign keys
|
||||||
|
* Fixed a crash in Django 5.2 when filtering against a composite primary key
|
||||||
|
using a tuple containing expressions
|
||||||
|
* Fixed a crash in Django 5.2 when validating a model that uses
|
||||||
|
GeneratedField or constraints composed of Q and Case lookups
|
||||||
|
* Added compatibility for docutils 0.22
|
||||||
|
* Fixed a crash in Django 5.2 when using a ManyToManyField on a model with
|
||||||
|
a composite primary key, by extending the fields.E347 system check
|
||||||
|
- Convert to libalternatives on SLE-16-based and newer systems
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Fri Aug 1 02:09:01 UTC 2025 - Steve Kowalik <steven.kowalik@suse.com>
|
||||||
|
|
||||||
|
- Reinstate Requires on tzdata, a lot of packages use it.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Wed Jul 30 06:03:44 UTC 2025 - Steve Kowalik <steven.kowalik@suse.com>
|
||||||
|
|
||||||
|
- Add patch support-msgfmt-0.25.patch:
|
||||||
|
* Support msgfmt 0.25 error messages changes. (bsc#1246966)
|
||||||
|
- Remove unneeded Requires on tzdata.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Sat Jul 19 06:51:37 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
Sat Jul 19 06:51:37 UTC 2025 - Markéta Machová <mmachova@suse.com>
|
||||||
|
|||||||
@@ -130,3 +130,55 @@ BzeRu0mAvjLkLNegQIvfdVXfYIcwUQQB8OAzoz3qzi8vji82MBQO+gkYrlteivoF
|
|||||||
z+gZLcBuv/NdNg==
|
z+gZLcBuv/NdNg==
|
||||||
=B8gH
|
=B8gH
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGV2bPUBEADQ/fSfsrdv/aGrUmyQ1ZmZNSDqlH8GXQH+LQiqOc5QjSgq09kv
|
||||||
|
YC5H3+yRmj4ENYJrD48Mce54k99U6LXhIopmPnCwnQjhpkXFqaZt8KtlhL/5VhIc
|
||||||
|
BFSodLkEFENWzpuxOkshOJYU8RxLO6MPQGKkokEGhxUHnNW5kwmGa93Iscu04SZR
|
||||||
|
L2pAHfYsnxB2Z5HAl4Os6iy7CKP8T5UQhGxwxojvjPyRJTb3IUDifjafPl4kbQyN
|
||||||
|
GdpgVnjHQtOzoVcXCsU4ig105IzydIf5BTYng3+idmlhQ1u8JzGETdlBvCtxg1K2
|
||||||
|
FmGD6IFzyvksdf44DNmxXmGnpMJK3y0+dpFzo33YBE3kFnYm57bwY55tdKmGnDyK
|
||||||
|
rR/1mARVOLQQYrDT6hgggNwRfqpTk28nU463LnXrLS89zaOxGQhfLm7w9kZ69WDd
|
||||||
|
UpI4RdZjA/klQJhPmLS7b8YmmZstvoZitsMgpPiexO+x7igMvWvuNyE8QL2EZ54K
|
||||||
|
6ojVObAdIbQe4LbiYCSut81F6e6tRsX3Jb46y5iUCZEkHvGsrZW6V1l/AIuYBecO
|
||||||
|
Xr5mmk5W4TpUlXTnyDFJr/FFtWfR1j2Z3ti+pZqAO4GUEUAjox+Dz/shKpmen7Qm
|
||||||
|
aj/Y/nuvD70B3c5XTWn62zTXVPYVX76sDnIuVsh/kORSP32OqMkesFKT9QARAQAB
|
||||||
|
tCdKYWNvYiBXYWxscyA8amFjb2J0eWxlcndhbGxzQGdtYWlsLmNvbT6JAlQEEwEI
|
||||||
|
AD4WIQRT1GlC4Aaio+7ci8gTFAP00W2NxwUCZXZs9QIbAwUJB4Ye+wULCQgHAgYV
|
||||||
|
CgkICwIEFgIDAQIeAQIXgAAKCRATFAP00W2NxwWVD/9ZTx8q3qsAXfOaWUNugaDM
|
||||||
|
oyUMa4bURfk82AXLVDju9fQQv/5wDlvQ+ejMDHE0pnvmQSa8bTol8y2IXO9Kl3ak
|
||||||
|
0NU3oOh3iJKk3q+FtxibGmIM5i095h2T97IBKJrZf6I1O0a9ZfMMDXbVpTeGIRDf
|
||||||
|
b55G6GG1jhdJa3SgtudUcGizlM0i7UrntUktmbnvWyYzbjWIKdYRq2W7vlEgKvNs
|
||||||
|
NZFVeO1KqLR92xUAIL+V7HtQ3TetyanYRH91Q8fZR9p+ScEOKJyfrpRa/IfrJKlg
|
||||||
|
k6MOqGeVaOd4nOX042S1+u0TU7HrVVVhL+Vru4utSYg9ZucM06wZOgR0A6Vay7Ap
|
||||||
|
Y43nGxRhc//auOJSh4nIQmtyup5+YiyKhynEnSF3pGuKXy64wVOO9XdW69bmyRxz
|
||||||
|
gAz4W5yTjmNBkQUYAGteJXo4fhDxWYzmeAIE7l5StTbgSZNLbndwmiWEyy8WzJtp
|
||||||
|
5xzluh+//39D+TUStZKG1MerhqnmJj3CBSTeeiKpW9QOtC4y2vuWzNp3XHS9FjeW
|
||||||
|
GOz5seJ5VDokkzP05XOfJ9SyblFAOW/Zteh2wdOb+S+/pUz+OukwPZthMwRsl0Dc
|
||||||
|
U+fx2mK+9I9TBJWgUHO1aXm6FlcyDnSlzc5oM4wXKF4sqByjjuCtYzK8UtEwlKNm
|
||||||
|
Pyboq4Y5RhYrW63LDl+SerkCDQRldmz1ARAAvkmAuvLXwZ86mubWYQFsmy3UKxdF
|
||||||
|
cewS8XikjsEZPvf0kYgi9wlMTRDvWlEmNGp9r4NWH36zm6uBjK9hxn+rNsiqkYez
|
||||||
|
MPTgwvxruSJWJPaFqK096Y8AMuk9SobMsydtlxuChQ3RwYo+j/v1csAdD88igzjW
|
||||||
|
3HKCzJ0e76ILdfbqlh8tJ+nbXvFgVVV2+vV996PTp4dQedc4/VzFGv00Mejkmhd3
|
||||||
|
nUBqdmG9mgqR4LK5xWCHXSVqgdVeH0urOiEN57aOie/30q6IegkfrKCSq1zGJE5o
|
||||||
|
WApzrOqo0UEDDND6PXDuLvzVuot3M/YvKjUXx8FatJC5BLSH0I9AKBWqPGvjk07V
|
||||||
|
IoI0CexyKoJKIkGl/ZUyRsorqmqg1671HUYNAFEbIwxeJUQkjTwqUxGXSjei7t8a
|
||||||
|
wzuZsA81lhXpwt6DfuQQ9XkDrc4dfyEuvkV9PforZxz8dVlDiORw3hPYqBjbU0hy
|
||||||
|
6D4Ytq3f88azYvZF8GE4uSYzvN6XeRhLr1hnfAFfGQFdNwg4qufjo8QYoQsYe6RZ
|
||||||
|
uenL1z5vkGRUz03l11GiaOqEJDJTy3dGACeQqAjjno8UL9Nb7PUWfnZL41zOGL8s
|
||||||
|
DHee7hX2+hsRPMoiqCzJrCjU52G5qGEB8d44VjNM8y435jyfBAh8ugv4rsSonzy+
|
||||||
|
ASe0msXK6bLxAy0AEQEAAYkCPAQYAQgAJhYhBFPUaULgBqKj7tyLyBMUA/TRbY3H
|
||||||
|
BQJldmz1AhsMBQkHhh77AAoJEBMUA/TRbY3HlA0QAI4DQ3MNfkpIZw/aKEetCaVB
|
||||||
|
LIVEfugoDcUQxiwc00G1aEuxpBdnfT5wFzUodUFJuIntGcYUMJwvAC3RB4ZANuoP
|
||||||
|
F2j/2JIp42GYhBjNSyy0ix/E2C/95HZDzsvDB4EZ0VMvec+MgAweTZhxZfdCsmId
|
||||||
|
lMWFfn3UM5FhtsOqLmw11T/6PdDgcyomrsp4ghHph9dJvMGiqXjysTSg3OMxzQeU
|
||||||
|
lzjX+6C4dY6AcpqAkeG+zRUYSQKsHQwSOxK7zon/hAA2+Gbvu+pzr0jHnatX8Fkd
|
||||||
|
eOBc6xZpnoQcubj/PXEBhXV4868jW/RNRG5ZXoQpnf63R0616JePkUKnXxs61JD9
|
||||||
|
oBuERUPn+HK346QNgNDRhzAufPYsXlPOPtU6poZqinMhsbcpW5Konjp2yLVwosXn
|
||||||
|
UPGLzZZHy9csF3VcWV96bM8WUEkFNfflisi1nE7yqC/CKgUjK0HUFDhyNHuCICTC
|
||||||
|
e9tvw2epbaLkid0NxV+a0RWWiUq2kMeTBUezVC27P+LAZ8aTZxHPyERtJlBgTCfm
|
||||||
|
1hWJ3CcIc4LT54AS3allzTv0f6iHX9WpJhfr3WPIY1Xt/d466l3VSofktwmpTEr2
|
||||||
|
DAECOit/CE1rFy1C+MHinAUX6C7ajg6msIE6uZHFrbHlSU8/WrbvZ1e4Nc7w7e/N
|
||||||
|
R182g26K1DaCZphtoq8Z
|
||||||
|
=0YqB
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# spec file for package python-Django
|
# spec file for package python-Django
|
||||||
#
|
#
|
||||||
# Copyright (c) 2025 SUSE LLC
|
# Copyright (c) 2026 SUSE LLC and contributors
|
||||||
#
|
#
|
||||||
# All modifications and additions to the file contributed by third parties
|
# All modifications and additions to the file contributed by third parties
|
||||||
# remain the property of their copyright owners, unless otherwise agreed
|
# remain the property of their copyright owners, unless otherwise agreed
|
||||||
@@ -20,8 +20,13 @@
|
|||||||
%bcond_with selenium
|
%bcond_with selenium
|
||||||
%bcond_with memcached
|
%bcond_with memcached
|
||||||
%{?sle15_python_module_pythons}
|
%{?sle15_python_module_pythons}
|
||||||
|
%if 0%{?suse_version} > 1500
|
||||||
|
%bcond_without libalternatives
|
||||||
|
%else
|
||||||
|
%bcond_with libalternatives
|
||||||
|
%endif
|
||||||
Name: python-Django
|
Name: python-Django
|
||||||
Version: 5.2.4
|
Version: 5.2.10
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: A high-level Python Web framework
|
Summary: A high-level Python Web framework
|
||||||
License: BSD-3-Clause
|
License: BSD-3-Clause
|
||||||
@@ -30,37 +35,11 @@ Source: https://www.djangoproject.com/m/releases/5.2/django-%{version}.t
|
|||||||
Source1: https://www.djangoproject.com/m/pgp/Django-%{version}.checksum.txt
|
Source1: https://www.djangoproject.com/m/pgp/Django-%{version}.checksum.txt
|
||||||
Source2: %{name}.keyring
|
Source2: %{name}.keyring
|
||||||
Source99: python-Django-rpmlintrc
|
Source99: python-Django-rpmlintrc
|
||||||
# PATCH-FIX-UPSTREAM https://github.com/django/django/pull/20390 Refs #36499 -- Adjusted test_strip_tags following Python behavior change for incomplete entities.
|
# PATCH-FIX-UPSTREAM https://github.com/django/django/pull/19530 Fixed #36421 -- Made test_msgfmt_error_including_non_ascii compatible with with msgfmt 0.25.
|
||||||
Patch100: test_strip_tags_incomplete.patch
|
Patch1: support-msgfmt-0.25.patch
|
||||||
# PATCH-FIX-UPSTREAM https://github.com/django/django/pull/19639 Fixed #36499 -- Adjusted utils_tests.test_html.TestUtilsHtml.test_strip_tags following Python's HTMLParser new behavior.
|
# PATCH-FIX-UPSTREAM https://github.com/django/django/pull/19639 Fixed #36499 -- Adjusted utils_tests.test_html.TestUtilsHtml.test_strip_tags following Python's HTMLParser new behavior.
|
||||||
# fixed and refined upstream, but some of our interpreters weren't updated to a new version yet and still only carry the patch, so providing the non-conditional version
|
# fixed and refined upstream, but some of our interpreters weren't updated to a new version yet and still only carry the patch, so providing the non-conditional version
|
||||||
Patch101: test_strip_tags.patch
|
Patch2: 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
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2025-59682.patch bsc#1250487
|
|
||||||
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
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2025-13473.patch bsc#1257401
|
|
||||||
Patch7: CVE-2025-13473.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2025-14550.patch bsc#1257403
|
|
||||||
Patch8: CVE-2025-14550.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2026-1207.patch bsc#1257405
|
|
||||||
Patch9: CVE-2026-1207.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2026-1285.patch bsc#1257406
|
|
||||||
Patch10: CVE-2026-1285.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2026-1287.patch bsc#1257407
|
|
||||||
Patch11: CVE-2026-1287.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2026-1312.patch bsc#1257408
|
|
||||||
Patch12: CVE-2026-1312.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2026-1312-followup.patch bsc#1257408
|
|
||||||
Patch13: CVE-2026-1312-followup.patch
|
|
||||||
BuildRequires: %{python_module Jinja2 >= 2.9.2}
|
BuildRequires: %{python_module Jinja2 >= 2.9.2}
|
||||||
BuildRequires: %{python_module Pillow >= 6.2.0}
|
BuildRequires: %{python_module Pillow >= 6.2.0}
|
||||||
BuildRequires: %{python_module PyYAML}
|
BuildRequires: %{python_module PyYAML}
|
||||||
@@ -85,8 +64,6 @@ Requires: python
|
|||||||
Requires: python-asgiref >= 3.7.0
|
Requires: python-asgiref >= 3.7.0
|
||||||
Requires: python-sqlparse >= 0.3.1
|
Requires: python-sqlparse >= 0.3.1
|
||||||
Requires: python-tzdata
|
Requires: python-tzdata
|
||||||
Requires(post): update-alternatives
|
|
||||||
Requires(postun): update-alternatives
|
|
||||||
Recommends: python-Jinja2 >= 2.9.2
|
Recommends: python-Jinja2 >= 2.9.2
|
||||||
Recommends: python-Pillow >= 6.2.0
|
Recommends: python-Pillow >= 6.2.0
|
||||||
Recommends: python-PyYAML
|
Recommends: python-PyYAML
|
||||||
@@ -100,6 +77,13 @@ Obsoletes: python-django < %{version}
|
|||||||
Provides: python-South = %{version}
|
Provides: python-South = %{version}
|
||||||
Obsoletes: python-South < %{version}
|
Obsoletes: python-South < %{version}
|
||||||
BuildArch: noarch
|
BuildArch: noarch
|
||||||
|
%if %{with libalternatives}
|
||||||
|
BuildRequires: alts
|
||||||
|
Requires: alts
|
||||||
|
%else
|
||||||
|
Requires(post): update-alternatives
|
||||||
|
Requires(postun): update-alternatives
|
||||||
|
%endif
|
||||||
%if %{with memcached}
|
%if %{with memcached}
|
||||||
BuildRequires: %{python_module pylibmc}
|
BuildRequires: %{python_module pylibmc}
|
||||||
BuildRequires: %{python_module pymemcache}
|
BuildRequires: %{python_module pymemcache}
|
||||||
@@ -160,6 +144,9 @@ export PATH=%{_libdir}/chromium:$PATH
|
|||||||
%python_expand PYTHONPATH=.:%{buildroot}%{$python_sitelib} $python tests/runtests.py -v 2
|
%python_expand PYTHONPATH=.:%{buildroot}%{$python_sitelib} $python tests/runtests.py -v 2
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
|
%pre
|
||||||
|
%python_libalternatives_reset_alternative django-admin
|
||||||
|
|
||||||
%post
|
%post
|
||||||
%{python_install_alternative django-admin}
|
%{python_install_alternative django-admin}
|
||||||
|
|
||||||
|
|||||||
69
support-msgfmt-0.25.patch
Normal file
69
support-msgfmt-0.25.patch
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
From 3609c463a4cfc5a7e76f4d4ba008c5096b1f1437 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Jericho Serrano <118679068+jericho1050@users.noreply.github.com>
|
||||||
|
Date: Fri, 6 Jun 2025 04:58:29 +0800
|
||||||
|
Subject: [PATCH] Fixed #36421 -- Made test_msgfmt_error_including_non_ascii
|
||||||
|
compatible with msgfmt 0.25.
|
||||||
|
|
||||||
|
---
|
||||||
|
tests/i18n/test_compilation.py | 25 +++++++++++++++++++++++--
|
||||||
|
1 file changed, 23 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/tests/i18n/test_compilation.py b/tests/i18n/test_compilation.py
|
||||||
|
index 4b0bb9f6bb1..3a57dbf0765 100644
|
||||||
|
--- a/tests/i18n/test_compilation.py
|
||||||
|
+++ b/tests/i18n/test_compilation.py
|
||||||
|
@@ -1,5 +1,6 @@
|
||||||
|
import gettext as gettext_module
|
||||||
|
import os
|
||||||
|
+import re
|
||||||
|
import stat
|
||||||
|
import unittest
|
||||||
|
from io import StringIO
|
||||||
|
@@ -8,10 +9,12 @@
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.core.management import CommandError, call_command, execute_from_command_line
|
||||||
|
-from django.core.management.utils import find_command
|
||||||
|
+from django.core.management.utils import find_command, popen_wrapper
|
||||||
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
from django.test.utils import captured_stderr, captured_stdout
|
||||||
|
from django.utils import translation
|
||||||
|
+from django.utils.encoding import DEFAULT_LOCALE_ENCODING
|
||||||
|
+from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext
|
||||||
|
|
||||||
|
from .utils import RunInTmpDirMixin, copytree
|
||||||
|
@@ -254,6 +257,17 @@ def test_no_dirs_accidentally_skipped(self):
|
||||||
|
|
||||||
|
|
||||||
|
class CompilationErrorHandling(MessageCompilationTests):
|
||||||
|
+ @cached_property
|
||||||
|
+ def msgfmt_version(self):
|
||||||
|
+ # Note that msgfmt is installed via GNU gettext tools, hence the msgfmt
|
||||||
|
+ # version should align to gettext.
|
||||||
|
+ out, err, status = popen_wrapper(
|
||||||
|
+ ["msgfmt", "--version"],
|
||||||
|
+ stdout_encoding=DEFAULT_LOCALE_ENCODING,
|
||||||
|
+ )
|
||||||
|
+ m = re.search(r"(\d+)\.(\d+)\.?(\d+)?", out)
|
||||||
|
+ return tuple(int(d) for d in m.groups() if d is not None)
|
||||||
|
+
|
||||||
|
def test_error_reported_by_msgfmt(self):
|
||||||
|
# po file contains wrong po formatting.
|
||||||
|
with self.assertRaises(CommandError):
|
||||||
|
@@ -278,7 +292,14 @@ def test_msgfmt_error_including_non_ascii(self):
|
||||||
|
call_command(
|
||||||
|
"compilemessages", locale=["ko"], stdout=StringIO(), stderr=stderr
|
||||||
|
)
|
||||||
|
- self.assertIn("' cannot start a field name", stderr.getvalue())
|
||||||
|
+ if self.msgfmt_version < (0, 25):
|
||||||
|
+ error_msg = "' cannot start a field name"
|
||||||
|
+ else:
|
||||||
|
+ error_msg = (
|
||||||
|
+ "a field name starts with a character that is not alphanumerical "
|
||||||
|
+ "or underscore"
|
||||||
|
+ )
|
||||||
|
+ self.assertIn(error_msg, stderr.getvalue())
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectAndAppTests(MessageCompilationTests):
|
||||||
@@ -10,10 +10,10 @@ Subject: [PATCH] Fixed #36499 -- Adjusted
|
|||||||
tests/utils_tests/test_html.py | 4 ++--
|
tests/utils_tests/test_html.py | 4 ++--
|
||||||
2 files changed, 4 insertions(+), 4 deletions(-)
|
2 files changed, 4 insertions(+), 4 deletions(-)
|
||||||
|
|
||||||
Index: django-5.2.4/tests/test_utils/tests.py
|
Index: django-5.2.9/tests/test_utils/tests.py
|
||||||
===================================================================
|
===================================================================
|
||||||
--- django-5.2.4.orig/tests/test_utils/tests.py
|
--- django-5.2.9.orig/tests/test_utils/tests.py
|
||||||
+++ django-5.2.4/tests/test_utils/tests.py
|
+++ django-5.2.9/tests/test_utils/tests.py
|
||||||
@@ -945,10 +945,10 @@ class HTMLEqualTests(SimpleTestCase):
|
@@ -945,10 +945,10 @@ class HTMLEqualTests(SimpleTestCase):
|
||||||
self.assertHTMLEqual("", "<p>")
|
self.assertHTMLEqual("", "<p>")
|
||||||
error_msg = (
|
error_msg = (
|
||||||
@@ -22,25 +22,28 @@ Index: django-5.2.4/tests/test_utils/tests.py
|
|||||||
+ "('Unexpected end tag `div` (Line 1, Column 0)', (1, 0))"
|
+ "('Unexpected end tag `div` (Line 1, Column 0)', (1, 0))"
|
||||||
)
|
)
|
||||||
with self.assertRaisesMessage(AssertionError, error_msg):
|
with self.assertRaisesMessage(AssertionError, error_msg):
|
||||||
- self.assertHTMLEqual("< div></ div>", "<div></div>")
|
- self.assertHTMLEqual("< div></div>", "<div></div>")
|
||||||
+ self.assertHTMLEqual("</div>", "<div></div>")
|
+ self.assertHTMLEqual("</div>", "<div></div>")
|
||||||
with self.assertRaises(HTMLParseError):
|
with self.assertRaises(HTMLParseError):
|
||||||
parse_html("</p>")
|
parse_html("</p>")
|
||||||
|
|
||||||
Index: django-5.2.4/tests/utils_tests/test_html.py
|
Index: django-5.2.9/tests/utils_tests/test_html.py
|
||||||
===================================================================
|
===================================================================
|
||||||
--- django-5.2.4.orig/tests/utils_tests/test_html.py
|
--- django-5.2.9.orig/tests/utils_tests/test_html.py
|
||||||
+++ django-5.2.4/tests/utils_tests/test_html.py
|
+++ django-5.2.9/tests/utils_tests/test_html.py
|
||||||
@@ -144,10 +144,10 @@ class TestUtilsHtml(SimpleTestCase):
|
@@ -187,13 +187,13 @@ class TestUtilsHtml(SimpleTestCase):
|
||||||
("&gotcha&#;<>", "&gotcha&#;<>"),
|
),
|
||||||
("<sc<!-- -->ript>test<<!-- -->/script>", "ript>test"),
|
(
|
||||||
("<script>alert()</script>&h", "alert()&h;"),
|
"><!" + ("&" * 16000) + "D",
|
||||||
- ("><!" + ("&" * 16000) + "D", "><!" + ("&" * 16000) + "D"),
|
- ">" if htmlparser_fixed_security else "><!" + ("&" * 16000) + "D",
|
||||||
+ ("><!" + ("&" * 16000) + "D", ">"),
|
+ ">",
|
||||||
|
),
|
||||||
("X<<<<br>br>br>br>X", "XX"),
|
("X<<<<br>br>br>br>X", "XX"),
|
||||||
("<" * 50 + "a>" * 50, ""),
|
("<" * 50 + "a>" * 50, ""),
|
||||||
- (">" + "<a" * 500 + "a", ">" + "<a" * 500 + "a"),
|
(
|
||||||
+ (">" + "<a" * 500 + "a", ">"),
|
">" + "<a" * 500 + "a",
|
||||||
|
- ">" if htmlparser_fixed_security else ">" + "<a" * 500 + "a",
|
||||||
|
+ ">",
|
||||||
|
),
|
||||||
("<a" * 49 + "a" * 951, "<a" * 49 + "a" * 951),
|
("<a" * 49 + "a" * 951, "<a" * 49 + "a" * 951),
|
||||||
("<" + "a" * 1_002, "<" + "a" * 1_002),
|
("<" + "a" * 1_002, "<" + "a" * 1_002),
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
From 5ca0f62213911a77dd4a62e843db7e420cc98b78 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Jacob Walls <jacobtylerwalls@gmail.com>
|
|
||||||
Date: Thu, 11 Dec 2025 08:44:19 -0500
|
|
||||||
Subject: [PATCH] [5.2.x] Refs #36499 -- Adjusted test_strip_tags following
|
|
||||||
Python behavior change for incomplete entities.
|
|
||||||
|
|
||||||
Backport of 7b80b2186300620931009fd62c2969f108fe7a62 from main.
|
|
||||||
---
|
|
||||||
tests/utils_tests/test_html.py | 35 +++++++++++++++++++++++++++++-----
|
|
||||||
1 file changed, 30 insertions(+), 5 deletions(-)
|
|
||||||
|
|
||||||
Index: django-5.2.4/tests/utils_tests/test_html.py
|
|
||||||
===================================================================
|
|
||||||
--- django-5.2.4.orig/tests/utils_tests/test_html.py
|
|
||||||
+++ django-5.2.4/tests/utils_tests/test_html.py
|
|
||||||
@@ -143,7 +144,7 @@ class TestUtilsHtml(SimpleTestCase):
|
|
||||||
# https://bugs.python.org/issue20288
|
|
||||||
("&gotcha&#;<>", "&gotcha&#;<>"),
|
|
||||||
("<sc<!-- -->ript>test<<!-- -->/script>", "ript>test"),
|
|
||||||
- ("<script>alert()</script>&h", "alert()h"),
|
|
||||||
+ ("<script>alert()</script>&h", "alert()&h;"),
|
|
||||||
("><!" + ("&" * 16000) + "D", "><!" + ("&" * 16000) + "D"),
|
|
||||||
("X<<<<br>br>br>br>X", "XX"),
|
|
||||||
("<" * 50 + "a>" * 50, ""),
|
|
||||||
Reference in New Issue
Block a user