Version in base suite: 4.2.23-1
Base version: python-django_4.2.23-1
Target version: python-django_4.2.27-0+deb13u1
Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-django/python-django_4.2.23-1.dsc
Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-django/python-django_4.2.27-0+deb13u1.dsc
Django.egg-info/PKG-INFO | 2
Django.egg-info/SOURCES.txt | 6
MANIFEST.in | 2
PKG-INFO | 2
debian/changelog | 49 ++
django/__init__.py | 2
django/core/serializers/xml_serializer.py | 39 +
django/db/backends/postgresql/compiler.py | 24 +
django/db/backends/postgresql/operations.py | 1
django/db/models/query.py | 5
django/db/models/query_utils.py | 4
django/db/models/sql/query.py | 9
django/http/response.py | 13
django/utils/archive.py | 6
django/utils/html.py | 3
django/utils/http.py | 2
docs/internals/contributing/writing-code/submitting-patches.txt | 4
docs/internals/contributing/writing-code/unit-tests.txt | 5
docs/ref/contrib/admin/admindocs.txt | 3
docs/releases/4.2.24.txt | 14
docs/releases/4.2.25.txt | 25 +
docs/releases/4.2.26.txt | 25 +
docs/releases/4.2.27.txt | 34 +
docs/releases/index.txt | 4
docs/releases/security.txt | 67 +++
docs/topics/serialization.txt | 2
scripts/manage_translations.py | 219 ----------
tests/aggregation/tests.py | 4
tests/annotations/tests.py | 51 ++
tests/expressions/test_queryset_values.py | 8
tests/gis_tests/gdal_tests/test_raster.py | 4
tests/gis_tests/relatedapp/tests.py | 13
tests/httpwrappers/tests.py | 15
tests/queries/test_q.py | 5
tests/queries/tests.py | 12
tests/requirements/py3.txt | 2
tests/serializers/test_xml.py | 55 ++
tests/test_runner/test_parallel.py | 8
tests/test_utils/tests.py | 2
tests/utils_tests/test_archive.py | 19
tests/utils_tests/test_html.py | 29 +
41 files changed, 529 insertions(+), 269 deletions(-)
dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpi9i1t5kh/python-django_4.2.23-1.dsc: no acceptable signature found
dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpi9i1t5kh/python-django_4.2.27-0+deb13u1.dsc: no acceptable signature found
diff -Nru python-django-4.2.23/Django.egg-info/PKG-INFO python-django-4.2.27/Django.egg-info/PKG-INFO
--- python-django-4.2.23/Django.egg-info/PKG-INFO 2025-06-10 09:58:08.000000000 +0000
+++ python-django-4.2.27/Django.egg-info/PKG-INFO 2025-12-02 12:46:18.000000000 +0000
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: Django
-Version: 4.2.23
+Version: 4.2.27
Summary: A high-level Python web framework that encourages rapid development and clean, pragmatic design.
Author-email: Django Software Foundation
License: BSD-3-Clause
diff -Nru python-django-4.2.23/Django.egg-info/SOURCES.txt python-django-4.2.27/Django.egg-info/SOURCES.txt
--- python-django-4.2.23/Django.egg-info/SOURCES.txt 2025-06-10 09:58:09.000000000 +0000
+++ python-django-4.2.27/Django.egg-info/SOURCES.txt 2025-12-02 12:46:18.000000000 +0000
@@ -3315,6 +3315,7 @@
django/db/backends/postgresql/__init__.py
django/db/backends/postgresql/base.py
django/db/backends/postgresql/client.py
+django/db/backends/postgresql/compiler.py
django/db/backends/postgresql/creation.py
django/db/backends/postgresql/features.py
django/db/backends/postgresql/introspection.py
@@ -4206,6 +4207,10 @@
docs/releases/4.2.21.txt
docs/releases/4.2.22.txt
docs/releases/4.2.23.txt
+docs/releases/4.2.24.txt
+docs/releases/4.2.25.txt
+docs/releases/4.2.26.txt
+docs/releases/4.2.27.txt
docs/releases/4.2.3.txt
docs/releases/4.2.4.txt
docs/releases/4.2.5.txt
@@ -4299,7 +4304,6 @@
js_tests/admin/jsi18n-mocks.test.js
js_tests/admin/navigation.test.js
js_tests/gis/mapwidget.test.js
-scripts/manage_translations.py
tests/.coveragerc
tests/README.rst
tests/runtests.py
diff -Nru python-django-4.2.23/MANIFEST.in python-django-4.2.27/MANIFEST.in
--- python-django-4.2.23/MANIFEST.in 2025-06-10 09:51:06.000000000 +0000
+++ python-django-4.2.27/MANIFEST.in 2025-12-02 12:43:50.000000000 +0000
@@ -10,6 +10,6 @@
graft docs
graft extras
graft js_tests
-graft scripts
graft tests
global-exclude *.py[co]
+prune scripts
diff -Nru python-django-4.2.23/PKG-INFO python-django-4.2.27/PKG-INFO
--- python-django-4.2.23/PKG-INFO 2025-06-10 09:58:09.710780100 +0000
+++ python-django-4.2.27/PKG-INFO 2025-12-02 12:46:19.003385000 +0000
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: Django
-Version: 4.2.23
+Version: 4.2.27
Summary: A high-level Python web framework that encourages rapid development and clean, pragmatic design.
Author-email: Django Software Foundation
License: BSD-3-Clause
diff -Nru python-django-4.2.23/debian/changelog python-django-4.2.27/debian/changelog
--- python-django-4.2.23/debian/changelog 2025-06-10 16:37:03.000000000 +0000
+++ python-django-4.2.27/debian/changelog 2026-01-23 18:43:29.000000000 +0000
@@ -1,3 +1,52 @@
+python-django (3:4.2.27-0+deb13u1) trixie-security; urgency=high
+
+ * New upstream security release:
+
+ - CVE-2025-13372: Fix a potential SQL injection attack in FilteredRelation
+ column aliases when using PostgreSQL. FilteredRelation was subject to SQL
+ injection in column aliases via a suitably crafted dictionary as the
+ **kwargs passed to QuerySet.annotate() or QuerySet.alias().
+
+ - CVE-2025-57833: Potential SQL injection in FilteredRelation column
+ aliases. The FilteredRelation feature in Django was subject to a
+ potential SQL injection vulnerability in column aliases that was
+ exploitable via suitably crafted dictionary with dictionary expansion as
+ the **kwargs passed QuerySet.annotate() or QuerySet.alias(). This CVE
+ was fixed in Django 4.2.24. (Closes: #1113865)
+
+ - CVE-2025-59681: Potential SQL injection in QuerySet.annotate(), alias(),
+ aggregate() and extra() on MySQL and MariaDB. QuerySet.annotate(),
+ QuerySet.alias(), QuerySet.aggregate() and QuerySet.extra() methods were
+ subject to SQL injection in column aliases, using a suitably crafted
+ dictionary with dictionary expansion as the **kwargs passed to these
+ methods on MySQL and MariaDB. This CVE was fixed in Django 4.2.25.
+
+ - CVE-2025-59682: Potential partial directory-traversal via
+ archive.extract(). The django.utils.archive.extract() function, used by
+ startapp --template and startproject --template allowed partial
+ directory-traversal via an archive with file paths sharing a common
+ prefix with the target directory. This CVE was fixed in Django 4.2.25.
+
+ - CVE-2025-64459: Prevent a potential SQL injection via _connector keyword
+ argument in QuerySet/Q objects. The methods QuerySet.filter(),
+ QuerySet.exclude(), and QuerySet.get() and the class Q() were subject to
+ SQL injection when using a suitably crafted dictionary (with dictionary
+ expansion) as the _connector argument. This CVE was fixed in Django
+ 4.2.26.
+
+ - CVE-2025-64460: Prevent a potential denial-of-service vulnerability in
+ XML serializer text extraction. An algorithmic complexity issue in
+ django.core.serializers.xml_serializer.getInnerText() allowed a remote
+ attacker to cause a potential denial-of-service triggering CPU and memory
+ exhaustion via a specially crafted XML input submitted to a service that
+ invokes XML Deserializer. The vulnerability resulted from repeated string
+ concatenation while recursively collecting text nodes, which produced
+ superlinear computation. (Closes: #1121788)
+
+
+
+ -- Chris Lamb Fri, 23 Jan 2026 10:43:29 -0800
+
python-django (3:4.2.23-1) unstable; urgency=high
* New upstream bugfix release. Quoting upstream:
diff -Nru python-django-4.2.23/django/__init__.py python-django-4.2.27/django/__init__.py
--- python-django-4.2.23/django/__init__.py 2025-06-10 09:51:53.000000000 +0000
+++ python-django-4.2.27/django/__init__.py 2025-12-02 12:45:49.000000000 +0000
@@ -1,6 +1,6 @@
from django.utils.version import get_version
-VERSION = (4, 2, 23, "final", 0)
+VERSION = (4, 2, 27, "final", 0)
__version__ = get_version(VERSION)
diff -Nru python-django-4.2.23/django/core/serializers/xml_serializer.py python-django-4.2.27/django/core/serializers/xml_serializer.py
--- python-django-4.2.23/django/core/serializers/xml_serializer.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/core/serializers/xml_serializer.py 2025-12-02 12:44:40.000000000 +0000
@@ -2,7 +2,8 @@
XML serializer.
"""
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
@@ -14,6 +15,25 @@
from django.utils.xmlutils import SimplerXMLGenerator, UnserializableContentError
+@contextmanager
+def fast_cache_clearing():
+ """Workaround for performance issues in minidom document checks.
+
+ Speeds up repeated DOM operations by skipping unnecessary full traversal
+ of the DOM tree.
+ """
+ module_helper_was_lambda = False
+ if original_fn := getattr(minidom, "_in_document", None):
+ module_helper_was_lambda = original_fn.__name__ == ""
+ if not module_helper_was_lambda:
+ minidom._in_document = lambda node: bool(node.ownerDocument)
+ try:
+ yield
+ finally:
+ if original_fn and not module_helper_was_lambda:
+ minidom._in_document = original_fn
+
+
class Serializer(base.Serializer):
"""Serialize a QuerySet to XML."""
@@ -208,7 +228,8 @@
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
@@ -392,19 +413,25 @@
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 -Nru python-django-4.2.23/django/db/backends/postgresql/compiler.py python-django-4.2.27/django/db/backends/postgresql/compiler.py
--- python-django-4.2.23/django/db/backends/postgresql/compiler.py 1970-01-01 00:00:00.000000000 +0000
+++ python-django-4.2.27/django/db/backends/postgresql/compiler.py 2025-12-02 12:44:19.000000000 +0000
@@ -0,0 +1,24 @@
+from django.db.models.sql.compiler import ( # isort:skip
+ SQLAggregateCompiler,
+ SQLCompiler as BaseSQLCompiler,
+ SQLDeleteCompiler,
+ SQLInsertCompiler,
+ SQLUpdateCompiler,
+)
+
+__all__ = [
+ "SQLAggregateCompiler",
+ "SQLCompiler",
+ "SQLDeleteCompiler",
+ "SQLInsertCompiler",
+ "SQLUpdateCompiler",
+]
+
+
+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)
diff -Nru python-django-4.2.23/django/db/backends/postgresql/operations.py python-django-4.2.27/django/db/backends/postgresql/operations.py
--- python-django-4.2.23/django/db/backends/postgresql/operations.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/db/backends/postgresql/operations.py 2025-12-02 12:44:19.000000000 +0000
@@ -23,6 +23,7 @@
class DatabaseOperations(BaseDatabaseOperations):
+ compiler_module = "django.db.backends.postgresql.compiler"
cast_char_field_without_max_length = "varchar"
explain_prefix = "EXPLAIN"
explain_options = frozenset(
diff -Nru python-django-4.2.23/django/db/models/query.py python-django-4.2.27/django/db/models/query.py
--- python-django-4.2.23/django/db/models/query.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/db/models/query.py 2025-12-02 12:43:50.000000000 +0000
@@ -42,6 +42,8 @@
# 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__(
@@ -1455,6 +1457,9 @@
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 -Nru python-django-4.2.23/django/db/models/query_utils.py python-django-4.2.27/django/db/models/query_utils.py
--- python-django-4.2.23/django/db/models/query_utils.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/db/models/query_utils.py 2025-12-02 12:43:50.000000000 +0000
@@ -44,8 +44,12 @@
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 -Nru python-django-4.2.23/django/db/models/sql/query.py python-django-4.2.27/django/db/models/sql/query.py
--- python-django-4.2.23/django/db/models/sql/query.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/db/models/sql/query.py 2025-12-02 12:43:50.000000000 +0000
@@ -46,9 +46,9 @@
__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
@@ -1123,8 +1123,8 @@
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):
@@ -1620,6 +1620,7 @@
return target_clause
def add_filtered_relation(self, filtered_relation, alias):
+ self.check_alias(alias)
filtered_relation.alias = alias
lookups = dict(get_children_from_q(filtered_relation.condition))
relation_lookup_parts, relation_field_parts, _ = self.solve_lookup_type(
diff -Nru python-django-4.2.23/django/http/response.py python-django-4.2.27/django/http/response.py
--- python-django-4.2.23/django/http/response.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/http/response.py 2025-12-02 12:43:50.000000000 +0000
@@ -21,7 +21,11 @@
from django.utils import timezone
from django.utils.datastructures import CaseInsensitiveMapping
from django.utils.encoding import iri_to_uri
-from django.utils.http import content_disposition_header, http_date
+from django.utils.http import (
+ MAX_URL_REDIRECT_LENGTH,
+ content_disposition_header,
+ http_date,
+)
from django.utils.regex_helper import _lazy_re_compile
_charset_from_content_type_re = _lazy_re_compile(
@@ -614,7 +618,12 @@
def __init__(self, redirect_to, *args, **kwargs):
super().__init__(*args, **kwargs)
self["Location"] = iri_to_uri(redirect_to)
- parsed = urlparse(str(redirect_to))
+ redirect_to_str = str(redirect_to)
+ if len(redirect_to_str) > MAX_URL_REDIRECT_LENGTH:
+ raise DisallowedRedirect(
+ f"Unsafe redirect exceeding {MAX_URL_REDIRECT_LENGTH} characters"
+ )
+ parsed = urlparse(redirect_to_str)
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
raise DisallowedRedirect(
"Unsafe redirect to URL with protocol '%s'" % parsed.scheme
diff -Nru python-django-4.2.23/django/utils/archive.py python-django-4.2.27/django/utils/archive.py
--- python-django-4.2.23/django/utils/archive.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/utils/archive.py 2025-12-02 12:43:50.000000000 +0000
@@ -144,7 +144,11 @@
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 -Nru python-django-4.2.23/django/utils/html.py python-django-4.2.27/django/utils/html.py
--- python-django-4.2.23/django/utils/html.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/utils/html.py 2025-12-02 12:43:50.000000000 +0000
@@ -9,12 +9,11 @@
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import punycode
from django.utils.functional import Promise, cached_property, keep_lazy, keep_lazy_text
-from django.utils.http import RFC3986_GENDELIMS, RFC3986_SUBDELIMS
+from django.utils.http import MAX_URL_LENGTH, RFC3986_GENDELIMS, RFC3986_SUBDELIMS
from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import SafeData, SafeString, mark_safe
from django.utils.text import normalize_newlines
-MAX_URL_LENGTH = 2048
MAX_STRIP_TAGS_DEPTH = 50
# HTML tag that opens but has no closing ">" after 1k+ chars.
diff -Nru python-django-4.2.23/django/utils/http.py python-django-4.2.27/django/utils/http.py
--- python-django-4.2.23/django/utils/http.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/django/utils/http.py 2025-12-02 12:43:50.000000000 +0000
@@ -47,6 +47,8 @@
RFC3986_GENDELIMS = ":/?#[]@"
RFC3986_SUBDELIMS = "!$&'()*+,;="
+MAX_URL_LENGTH = 2048
+MAX_URL_REDIRECT_LENGTH = 16384
# TODO: Remove when dropping support for PY38.
# Unsafe bytes to be removed per WHATWG spec.
diff -Nru python-django-4.2.23/docs/internals/contributing/writing-code/submitting-patches.txt python-django-4.2.27/docs/internals/contributing/writing-code/submitting-patches.txt
--- python-django-4.2.23/docs/internals/contributing/writing-code/submitting-patches.txt 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/docs/internals/contributing/writing-code/submitting-patches.txt 2025-12-02 12:43:50.000000000 +0000
@@ -320,8 +320,8 @@
* Does the :doc:`coding style
` conform to our
- guidelines? Are there any ``black``, ``blacken-docs``, ``flake8``, or
- ``isort`` errors? You can install the :ref:`pre-commit
+ guidelines? Are there any ``black``, ``blacken-docs``, ``flake8``,
+ ``isort``, or ``zizmor`` errors? You can install the :ref:`pre-commit
` hooks to automatically catch these errors.
* If the change is backwards incompatible in any way, is there a note
in the release notes (``docs/releases/A.B.txt``)?
diff -Nru python-django-4.2.23/docs/internals/contributing/writing-code/unit-tests.txt python-django-4.2.27/docs/internals/contributing/writing-code/unit-tests.txt
--- python-django-4.2.23/docs/internals/contributing/writing-code/unit-tests.txt 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/docs/internals/contributing/writing-code/unit-tests.txt 2025-12-02 12:43:50.000000000 +0000
@@ -69,7 +69,7 @@
$ tox
By default, ``tox`` runs the test suite with the bundled test settings file for
-SQLite, ``black``, ``blacken-docs``, ``flake8``, ``isort``, and the
+SQLite, ``black``, ``blacken-docs``, ``flake8``, ``isort``, ``zizmor``, and the
documentation spelling checker. In addition to the system dependencies noted
elsewhere in this documentation, the command ``python3`` must be on your path
and linked to the appropriate version of Python. A list of default environments
@@ -84,6 +84,7 @@
flake8>=3.7.0
docs
isort>=5.1.0
+ zizmor>=1.16.3
Testing other Python versions and database backends
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -284,7 +285,7 @@
* :pypi:`asgiref` 3.6.0+ (required)
* :pypi:`bcrypt`
* :pypi:`colorama`
-* :pypi:`docutils`
+* :pypi:`docutils` <0.22
* :pypi:`geoip2`
* :pypi:`Jinja2` 2.11+
* :pypi:`numpy`
diff -Nru python-django-4.2.23/docs/ref/contrib/admin/admindocs.txt python-django-4.2.27/docs/ref/contrib/admin/admindocs.txt
--- python-django-4.2.23/docs/ref/contrib/admin/admindocs.txt 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/docs/ref/contrib/admin/admindocs.txt 2025-12-02 12:43:50.000000000 +0000
@@ -23,7 +23,8 @@
your ``urlpatterns``. Make sure it's included *before* the
``'admin/'`` entry, so that requests to ``/admin/doc/`` don't get
handled by the latter entry.
-* Install the docutils Python module (https://docutils.sourceforge.io/).
+* Install the docutils Python module version <0.22
+ (https://docutils.sourceforge.io/).
* **Optional:** Using the admindocs bookmarklets requires
``django.contrib.admindocs.middleware.XViewMiddleware`` to be installed.
diff -Nru python-django-4.2.23/docs/releases/4.2.24.txt python-django-4.2.27/docs/releases/4.2.24.txt
--- python-django-4.2.23/docs/releases/4.2.24.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-4.2.27/docs/releases/4.2.24.txt 2025-10-22 18:22:16.000000000 +0000
@@ -0,0 +1,14 @@
+===========================
+Django 4.2.24 release notes
+===========================
+
+*September 3, 2025*
+
+Django 4.2.24 fixes a security issue with severity "high" in 4.2.23.
+
+CVE-2025-57833: Potential SQL injection in ``FilteredRelation`` column aliases
+==============================================================================
+
+:class:`.FilteredRelation` was subject to SQL injection in column aliases,
+using a suitably crafted dictionary, with dictionary expansion, as the
+``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias`.
diff -Nru python-django-4.2.23/docs/releases/4.2.25.txt python-django-4.2.27/docs/releases/4.2.25.txt
--- python-django-4.2.23/docs/releases/4.2.25.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-4.2.27/docs/releases/4.2.25.txt 2025-10-22 18:22:16.000000000 +0000
@@ -0,0 +1,25 @@
+===========================
+Django 4.2.25 release notes
+===========================
+
+*October 1, 2025*
+
+Django 4.2.25 fixes one security issue with severity "high" and one security
+issue with severity "low" in 4.2.24.
+
+CVE-2025-59681: Potential SQL injection in ``QuerySet.annotate()``, ``alias()``, ``aggregate()``, and ``extra()`` on MySQL and MariaDB
+======================================================================================================================================
+
+:meth:`.QuerySet.annotate`, :meth:`~.QuerySet.alias`,
+:meth:`~.QuerySet.aggregate`, and :meth:`~.QuerySet.extra` methods were subject
+to SQL injection in column aliases, using a suitably crafted dictionary, with
+dictionary expansion, as the ``**kwargs`` passed to these methods (follow up to
+:cve:`2022-28346`).
+
+CVE-2025-59682: Potential partial directory-traversal via ``archive.extract()``
+===============================================================================
+
+The ``django.utils.archive.extract()`` function, used by
+:option:`startapp --template` and :option:`startproject --template`, allowed
+partial directory-traversal via an archive with file paths sharing a common
+prefix with the target directory (follow up to :cve:`2021-3281`).
diff -Nru python-django-4.2.23/docs/releases/4.2.26.txt python-django-4.2.27/docs/releases/4.2.26.txt
--- python-django-4.2.23/docs/releases/4.2.26.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-4.2.27/docs/releases/4.2.26.txt 2025-11-05 12:53:18.000000000 +0000
@@ -0,0 +1,25 @@
+===========================
+Django 4.2.26 release notes
+===========================
+
+*November 5, 2025*
+
+Django 4.2.26 fixes one security issue with severity "high" and one security
+issue with severity "moderate" in 4.2.25.
+
+CVE-2025-64458: Potential denial-of-service vulnerability in ``HttpResponseRedirect`` and ``HttpResponsePermanentRedirect`` on Windows
+======================================================================================================================================
+
+Python's :func:`NFKC normalization ` is slow on
+Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`,
+:class:`~django.http.HttpResponsePermanentRedirect`, and the shortcut
+:func:`redirect() ` were subject to a potential
+denial-of-service attack via certain inputs with a very large number of Unicode
+characters (follow up to :cve:`2025-27556`).
+
+CVE-2025-64459: Potential SQL injection via ``_connector`` keyword argument
+===========================================================================
+
+:meth:`.QuerySet.filter`, :meth:`~.QuerySet.exclude`, :meth:`~.QuerySet.get`,
+and :class:`~.Q` were subject to SQL injection using a suitably crafted
+dictionary, with dictionary expansion, as the ``_connector`` argument.
diff -Nru python-django-4.2.23/docs/releases/4.2.27.txt python-django-4.2.27/docs/releases/4.2.27.txt
--- python-django-4.2.23/docs/releases/4.2.27.txt 1970-01-01 00:00:00.000000000 +0000
+++ python-django-4.2.27/docs/releases/4.2.27.txt 2025-12-02 12:44:40.000000000 +0000
@@ -0,0 +1,34 @@
+===========================
+Django 4.2.27 release notes
+===========================
+
+*December 2, 2025*
+
+Django 4.2.27 fixes one security issue with severity "high", one security issue
+with severity "moderate", and one bug in 4.2.26.
+
+CVE-2025-13372: Potential SQL injection in ``FilteredRelation`` column aliases on PostgreSQL
+============================================================================================
+
+:class:`.FilteredRelation` was subject to SQL injection in column aliases,
+using a suitably crafted dictionary, with dictionary expansion, as the
+``**kwargs`` passed to :meth:`.QuerySet.annotate` or :meth:`.QuerySet.alias` on
+PostgreSQL.
+
+CVE-2025-64460: Potential denial-of-service vulnerability in XML ``Deserializer``
+=================================================================================
+
+:ref:`XML Serialization ` was subject to a potential
+denial-of-service attack due to quadratic time complexity when deserializing
+crafted documents containing many nested invalid elements. The internal helper
+``django.core.serializers.xml_serializer.getInnerText()`` previously
+accumulated inner text inefficiently during recursion. It now collects text per
+element, avoiding excessive resource usage.
+
+Bugfixes
+========
+
+* Fixed a regression in Django 4.2.26 where ``DisallowedRedirect`` was raised
+ by :class:`~django.http.HttpResponseRedirect` and
+ :class:`~django.http.HttpResponsePermanentRedirect` for URLs longer than 2048
+ characters. The limit is now 16384 characters (:ticket:`36743`).
diff -Nru python-django-4.2.23/docs/releases/index.txt python-django-4.2.27/docs/releases/index.txt
--- python-django-4.2.23/docs/releases/index.txt 2025-06-10 09:51:08.000000000 +0000
+++ python-django-4.2.27/docs/releases/index.txt 2025-12-02 12:43:50.000000000 +0000
@@ -26,6 +26,10 @@
.. toctree::
:maxdepth: 1
+ 4.2.27
+ 4.2.26
+ 4.2.25
+ 4.2.24
4.2.23
4.2.22
4.2.21
diff -Nru python-django-4.2.23/docs/releases/security.txt python-django-4.2.27/docs/releases/security.txt
--- python-django-4.2.23/docs/releases/security.txt 2025-06-10 09:51:08.000000000 +0000
+++ python-django-4.2.27/docs/releases/security.txt 2025-12-02 12:43:50.000000000 +0000
@@ -36,6 +36,65 @@
All security issues have been handled under versions of Django's security
process. These are listed below.
+November 5, 2025 - :cve:`2025-64458`
+------------------------------------
+
+Potential denial-of-service vulnerability in ``HttpResponseRedirect`` and
+``HttpResponsePermanentRedirect`` on Windows. `Full description
+`__
+
+* Django 6.0 :commit:`(patch) <6e13348436fccf8f22982921d6a3a3e65c956a9f>`
+* Django 5.2 :commit:`(patch) <4f5d904b63751dea9ffc3b0e046404a7fa5881ac>`
+* Django 5.1 :commit:`(patch) <3790593781d26168e7306b5b2f8ea0309de16242>`
+* Django 4.2 :commit:`(patch) <770eea38d7a0e9ba9455140b5a9a9e33618226a7>`
+
+November 5, 2025 - :cve:`2025-64459`
+------------------------------------
+
+Potential SQL injection via ``_connector`` keyword argument in ``QuerySet`` and
+``Q`` objects. `Full description
+`__
+
+* Django 6.0 :commit:`(patch) <06dd38324ac3d60d83d9f3adabf0dcdf423d2a85>`
+* Django 5.2 :commit:`(patch) <6703f364d767e949c5b0e4016433ef75063b4f9b>`
+* Django 5.1 :commit:`(patch) <72d2c87431f2ae0431d65d0ec792047f078c8241>`
+* Django 4.2 :commit:`(patch) <59ae82e67053d281ff4562a24bbba21299f0a7d4>`
+
+October 1, 2025 - :cve:`2025-59681`
+-----------------------------------
+
+Potential SQL injection in ``QuerySet.annotate()``, ``alias()``,
+``aggregate()``, and ``extra()`` on MySQL and MariaDB. `Full description
+`__
+
+* Django 6.0 :commit:`(patch) <4ceaaee7e04b416fc465e838a6ef43ca0ccffafe>`
+* Django 5.2 :commit:`(patch) <52fbae0a4dbbe5faa59827f8f05694a0065cc135>`
+* Django 5.1 :commit:`(patch) <01d2d770e22bffe53c7f1e611e2bbca94cb8a2e7>`
+* Django 4.2 :commit:`(patch) <38d9ef8c7b5cb6ef51b933e51a20e0e0063f33d5>`
+
+October 1, 2025 - :cve:`2025-59682`
+-----------------------------------
+
+Potential partial directory-traversal via ``archive.extract()``.
+`Full description
+`__
+
+* Django 6.0 :commit:`(patch) `
+* Django 5.2 :commit:`(patch) `
+* Django 5.1 :commit:`(patch) <74fa85c688a87224637155902bcd738bb9e65e11>`
+* Django 4.2 :commit:`(patch) <9504bbaa392c9fe37eee9291f5b4c29eb6037619>`
+
+September 3, 2025 - :cve:`2025-57833`
+-------------------------------------
+
+Potential SQL injection in ``FilteredRelation`` column aliases.
+`Full description
+`__
+
+* Django 5.2 :commit:`(patch) <4c044fcc866ec226f612c475950b690b0139d243>`
+* Django 5.1 :commit:`(patch) <102965ea93072fe3c39a30be437c683ec1106ef5>`
+* Django 4.2 :commit:`(patch) <31334e6965ad136a5e369993b01721499c5d1a92>`
+
June 4, 2025 - :cve:`2025-48432`
--------------------------------
@@ -47,6 +106,14 @@
* Django 5.1 :commit:`(patch) <596542ddb46cdabe011322917e1655f0d24eece2>`
* Django 4.2 :commit:`(patch) `
+There was an additional hardening with new patch releases published on June 10,
+2025. `Full description
+`__
+
+* Django 5.2.3 :commit:`(patch) <8fcc83953c350e158a484bf1da0aa1b79b69bb07>`
+* Django 5.1.11 :commit:`(patch) <31f4bd31fa16f7f5302f65b9b8b7a49b69a7c4a6>`
+* Django 4.2.23 :commit:`(patch) `
+
May 7, 2025 - :cve:`2025-32873`
-------------------------------
diff -Nru python-django-4.2.23/docs/topics/serialization.txt python-django-4.2.27/docs/topics/serialization.txt
--- python-django-4.2.23/docs/topics/serialization.txt 2025-06-10 09:47:17.000000000 +0000
+++ python-django-4.2.27/docs/topics/serialization.txt 2025-12-02 12:44:40.000000000 +0000
@@ -173,6 +173,8 @@
.. _jsonl: https://jsonlines.org/
.. _PyYAML: https://pyyaml.org/
+.. _serialization-formats-xml:
+
XML
---
diff -Nru python-django-4.2.23/scripts/manage_translations.py python-django-4.2.27/scripts/manage_translations.py
--- python-django-4.2.23/scripts/manage_translations.py 2025-06-10 09:47:17.000000000 +0000
+++ python-django-4.2.27/scripts/manage_translations.py 1970-01-01 00:00:00.000000000 +0000
@@ -1,219 +0,0 @@
-#!/usr/bin/env python
-#
-# This Python file contains utility scripts to manage Django translations.
-# It has to be run inside the django git root directory.
-#
-# The following commands are available:
-#
-# * update_catalogs: check for new strings in core and contrib catalogs, and
-# output how much strings are new/changed.
-#
-# * lang_stats: output statistics for each catalog/language combination
-#
-# * fetch: fetch translations from transifex.com
-#
-# Each command support the --languages and --resources options to limit their
-# operation to the specified language or resource. For example, to get stats
-# for Spanish in contrib.admin, run:
-#
-# $ python scripts/manage_translations.py lang_stats --language=es --resources=admin
-
-import os
-from argparse import ArgumentParser
-from subprocess import run
-
-import django
-from django.conf import settings
-from django.core.management import call_command
-
-HAVE_JS = ["admin"]
-
-
-def _get_locale_dirs(resources, include_core=True):
- """
- Return a tuple (contrib name, absolute path) for all locale directories,
- optionally including the django core catalog.
- If resources list is not None, filter directories matching resources content.
- """
- contrib_dir = os.path.join(os.getcwd(), "django", "contrib")
- dirs = []
-
- # Collect all locale directories
- for contrib_name in os.listdir(contrib_dir):
- path = os.path.join(contrib_dir, contrib_name, "locale")
- if os.path.isdir(path):
- dirs.append((contrib_name, path))
- if contrib_name in HAVE_JS:
- dirs.append(("%s-js" % contrib_name, path))
- if include_core:
- dirs.insert(0, ("core", os.path.join(os.getcwd(), "django", "conf", "locale")))
-
- # Filter by resources, if any
- if resources is not None:
- res_names = [d[0] for d in dirs]
- dirs = [ld for ld in dirs if ld[0] in resources]
- if len(resources) > len(dirs):
- print(
- "You have specified some unknown resources. "
- "Available resource names are: %s" % (", ".join(res_names),)
- )
- exit(1)
- return dirs
-
-
-def _tx_resource_for_name(name):
- """Return the Transifex resource name"""
- if name == "core":
- return "django.core"
- else:
- return "django.contrib-%s" % name
-
-
-def _check_diff(cat_name, base_path):
- """
- Output the approximate number of changed/added strings in the en catalog.
- """
- po_path = "%(path)s/en/LC_MESSAGES/django%(ext)s.po" % {
- "path": base_path,
- "ext": "js" if cat_name.endswith("-js") else "",
- }
- p = run(
- "git diff -U0 %s | egrep '^[-+]msgid' | wc -l" % po_path,
- capture_output=True,
- shell=True,
- )
- num_changes = int(p.stdout.strip())
- print("%d changed/added messages in '%s' catalog." % (num_changes, cat_name))
-
-
-def update_catalogs(resources=None, languages=None):
- """
- Update the en/LC_MESSAGES/django.po (main and contrib) files with
- new/updated translatable strings.
- """
- settings.configure()
- django.setup()
- if resources is not None:
- print("`update_catalogs` will always process all resources.")
- contrib_dirs = _get_locale_dirs(None, include_core=False)
-
- os.chdir(os.path.join(os.getcwd(), "django"))
- print("Updating en catalogs for Django and contrib apps...")
- call_command("makemessages", locale=["en"])
- print("Updating en JS catalogs for Django and contrib apps...")
- call_command("makemessages", locale=["en"], domain="djangojs")
-
- # Output changed stats
- _check_diff("core", os.path.join(os.getcwd(), "conf", "locale"))
- for name, dir_ in contrib_dirs:
- _check_diff(name, dir_)
-
-
-def lang_stats(resources=None, languages=None):
- """
- Output language statistics of committed translation files for each
- Django catalog.
- If resources is provided, it should be a list of translation resource to
- limit the output (e.g. ['core', 'gis']).
- """
- locale_dirs = _get_locale_dirs(resources)
-
- for name, dir_ in locale_dirs:
- print("\nShowing translations stats for '%s':" % name)
- langs = sorted(d for d in os.listdir(dir_) if not d.startswith("_"))
- for lang in langs:
- if languages and lang not in languages:
- continue
- # TODO: merge first with the latest en catalog
- po_path = "{path}/{lang}/LC_MESSAGES/django{ext}.po".format(
- path=dir_, lang=lang, ext="js" if name.endswith("-js") else ""
- )
- p = run(
- ["msgfmt", "-vc", "-o", "/dev/null", po_path],
- capture_output=True,
- env={"LANG": "C"},
- encoding="utf-8",
- )
- if p.returncode == 0:
- # msgfmt output stats on stderr
- print("%s: %s" % (lang, p.stderr.strip()))
- else:
- print(
- "Errors happened when checking %s translation for %s:\n%s"
- % (lang, name, p.stderr)
- )
-
-
-def fetch(resources=None, languages=None):
- """
- Fetch translations from Transifex, wrap long lines, generate mo files.
- """
- locale_dirs = _get_locale_dirs(resources)
- errors = []
-
- for name, dir_ in locale_dirs:
- # Transifex pull
- if languages is None:
- run(
- [
- "tx",
- "pull",
- "-r",
- _tx_resource_for_name(name),
- "-a",
- "-f",
- "--minimum-perc=5",
- ]
- )
- target_langs = sorted(
- d for d in os.listdir(dir_) if not d.startswith("_") and d != "en"
- )
- else:
- for lang in languages:
- run(["tx", "pull", "-r", _tx_resource_for_name(name), "-f", "-l", lang])
- target_langs = languages
-
- # msgcat to wrap lines and msgfmt for compilation of .mo file
- for lang in target_langs:
- po_path = "%(path)s/%(lang)s/LC_MESSAGES/django%(ext)s.po" % {
- "path": dir_,
- "lang": lang,
- "ext": "js" if name.endswith("-js") else "",
- }
- if not os.path.exists(po_path):
- print(
- "No %(lang)s translation for resource %(name)s"
- % {"lang": lang, "name": name}
- )
- continue
- run(["msgcat", "--no-location", "-o", po_path, po_path])
- msgfmt = run(["msgfmt", "-c", "-o", "%s.mo" % po_path[:-3], po_path])
- if msgfmt.returncode != 0:
- errors.append((name, lang))
- if errors:
- print("\nWARNING: Errors have occurred in following cases:")
- for resource, lang in errors:
- print("\tResource %s for language %s" % (resource, lang))
- exit(1)
-
-
-if __name__ == "__main__":
- RUNABLE_SCRIPTS = ("update_catalogs", "lang_stats", "fetch")
-
- parser = ArgumentParser()
- parser.add_argument("cmd", nargs=1, choices=RUNABLE_SCRIPTS)
- parser.add_argument(
- "-r",
- "--resources",
- action="append",
- help="limit operation to the specified resources",
- )
- parser.add_argument(
- "-l",
- "--languages",
- action="append",
- help="limit operation to the specified languages",
- )
- options = parser.parse_args()
-
- eval(options.cmd[0])(options.resources, options.languages)
diff -Nru python-django-4.2.23/tests/aggregation/tests.py python-django-4.2.27/tests/aggregation/tests.py
--- python-django-4.2.23/tests/aggregation/tests.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/aggregation/tests.py 2025-12-02 12:43:50.000000000 +0000
@@ -2090,8 +2090,8 @@
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 -Nru python-django-4.2.23/tests/annotations/tests.py python-django-4.2.27/tests/annotations/tests.py
--- python-django-4.2.23/tests/annotations/tests.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/annotations/tests.py 2025-12-02 12:44:19.000000000 +0000
@@ -2,6 +2,7 @@
from decimal import Decimal
from django.core.exceptions import FieldDoesNotExist, FieldError
+from django.db import connection
from django.db.models import (
BooleanField,
Case,
@@ -12,6 +13,7 @@
Exists,
ExpressionWrapper,
F,
+ FilteredRelation,
FloatField,
Func,
IntegerField,
@@ -1115,12 +1117,21 @@
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)})
+ 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")})
+
def test_alias_forbidden_chars(self):
tests = [
'al"ias',
@@ -1133,19 +1144,25 @@
"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):
with self.assertRaisesMessage(ValueError, msg):
Book.objects.annotate(**{crafted_alias: Value(1)})
+ with self.assertRaisesMessage(ValueError, msg):
+ Book.objects.annotate(
+ **{crafted_alias: FilteredRelation("authors")}
+ )
+
class AliasTests(TestCase):
@classmethod
@@ -1413,8 +1430,28 @@
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)})
+
+ 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")})
+
+ def test_alias_filtered_relation_sql_injection_dollar_sign(self):
+ qs = Book.objects.alias(
+ **{"crafted_alia$": FilteredRelation("authors")}
+ ).values("name", "crafted_alia$")
+ if connection.vendor == "postgresql":
+ msg = "Dollar signs are not permitted in column aliases on PostgreSQL."
+ with self.assertRaisesMessage(ValueError, msg):
+ list(qs)
+ else:
+ self.assertEqual(qs.first()["name"], self.b1.name)
diff -Nru python-django-4.2.23/tests/expressions/test_queryset_values.py python-django-4.2.27/tests/expressions/test_queryset_values.py
--- python-django-4.2.23/tests/expressions/test_queryset_values.py 2025-05-22 13:21:16.000000000 +0000
+++ python-django-4.2.27/tests/expressions/test_queryset_values.py 2025-12-02 12:30:32.000000000 +0000
@@ -37,8 +37,8 @@
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 @@
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 -Nru python-django-4.2.23/tests/gis_tests/gdal_tests/test_raster.py python-django-4.2.27/tests/gis_tests/gdal_tests/test_raster.py
--- python-django-4.2.23/tests/gis_tests/gdal_tests/test_raster.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/gis_tests/gdal_tests/test_raster.py 2025-12-02 12:43:50.000000000 +0000
@@ -6,7 +6,7 @@
from pathlib import Path
from unittest import mock
-from django.contrib.gis.gdal import GDALRaster, SpatialReference
+from django.contrib.gis.gdal import GDAL_VERSION, GDALRaster, SpatialReference
from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.gdal.raster.band import GDALBand
from django.contrib.gis.shortcuts import numpy
@@ -406,6 +406,8 @@
self.assertIn("NAD83 / Florida GDL Albers", infos)
def test_compressed_file_based_raster_creation(self):
+ if GDAL_VERSION > (3, 4):
+ self.skipTest("GDAL_PIXEL_TYPES are missing types from GDAL 3.5+.")
rstfile = tempfile.NamedTemporaryFile(suffix=".tif")
# Make a compressed copy of an existing raster.
compressed = self.rs.warp(
diff -Nru python-django-4.2.23/tests/gis_tests/relatedapp/tests.py python-django-4.2.27/tests/gis_tests/relatedapp/tests.py
--- python-django-4.2.23/tests/gis_tests/relatedapp/tests.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/gis_tests/relatedapp/tests.py 2025-12-02 12:43:50.000000000 +0000
@@ -98,10 +98,15 @@
self.assertEqual(type(u3), MultiPoint)
# Ordering of points in the result of the union is not defined and
- # implementation-dependent (DB backend, GEOS version)
- self.assertEqual({p.ewkt for p in ref_u1}, {p.ewkt for p in u1})
- self.assertEqual({p.ewkt for p in ref_u2}, {p.ewkt for p in u2})
- self.assertEqual({p.ewkt for p in ref_u1}, {p.ewkt for p in u3})
+ # implementation-dependent (DB backend, GEOS version).
+ tests = [
+ (u1, ref_u1),
+ (u2, ref_u2),
+ (u3, ref_u1),
+ ]
+ for union, ref in tests:
+ for point, ref_point in zip(sorted(union), sorted(ref)):
+ self.assertIs(point.equals_exact(ref_point, tolerance=6), True)
def test05_select_related_fk_to_subclass(self):
"""
diff -Nru python-django-4.2.23/tests/httpwrappers/tests.py python-django-4.2.27/tests/httpwrappers/tests.py
--- python-django-4.2.23/tests/httpwrappers/tests.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/httpwrappers/tests.py 2025-12-02 12:43:50.000000000 +0000
@@ -24,6 +24,7 @@
)
from django.test import SimpleTestCase
from django.utils.functional import lazystr
+from django.utils.http import MAX_URL_REDIRECT_LENGTH
class QueryDictTests(SimpleTestCase):
@@ -485,11 +486,25 @@
r.writelines(["foo\n", "bar\n", "baz\n"])
self.assertEqual(r.content, b"foo\nbar\nbaz\n")
+ def test_redirect_url_max_length(self):
+ base_url = "https://example.com/"
+ for length in (
+ MAX_URL_REDIRECT_LENGTH - 1,
+ MAX_URL_REDIRECT_LENGTH,
+ ):
+ long_url = base_url + "x" * (length - len(base_url))
+ with self.subTest(length=length):
+ response = HttpResponseRedirect(long_url)
+ self.assertEqual(response.url, long_url)
+ response = HttpResponsePermanentRedirect(long_url)
+ self.assertEqual(response.url, long_url)
+
def test_unsafe_redirect(self):
bad_urls = [
'data:text/html,',
"mailto:test@example.com",
"file:///etc/passwd",
+ "é" * (MAX_URL_REDIRECT_LENGTH + 1),
]
for url in bad_urls:
with self.assertRaises(DisallowedRedirect):
diff -Nru python-django-4.2.23/tests/queries/test_q.py python-django-4.2.27/tests/queries/test_q.py
--- python-django-4.2.23/tests/queries/test_q.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/queries/test_q.py 2025-12-02 12:43:50.000000000 +0000
@@ -225,6 +225,11 @@
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")
+
class QCheckTests(TestCase):
def test_basic(self):
diff -Nru python-django-4.2.23/tests/queries/tests.py python-django-4.2.27/tests/queries/tests.py
--- python-django-4.2.23/tests/queries/tests.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/queries/tests.py 2025-12-02 12:43:50.000000000 +0000
@@ -1943,8 +1943,8 @@
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"})
@@ -4490,6 +4490,14 @@
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):
"""
diff -Nru python-django-4.2.23/tests/requirements/py3.txt python-django-4.2.27/tests/requirements/py3.txt
--- python-django-4.2.23/tests/requirements/py3.txt 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/requirements/py3.txt 2025-12-02 12:43:50.000000000 +0000
@@ -4,7 +4,7 @@
backports.zoneinfo; python_version < '3.9'
bcrypt
black == 23.12.1
-docutils
+docutils < 0.22
geoip2; python_version < '3.12'
jinja2 >= 2.11.0
numpy; python_version < '3.12'
diff -Nru python-django-4.2.23/tests/serializers/test_xml.py python-django-4.2.27/tests/serializers/test_xml.py
--- python-django-4.2.23/tests/serializers/test_xml.py 2025-06-10 09:47:17.000000000 +0000
+++ python-django-4.2.27/tests/serializers/test_xml.py 2025-12-02 12:44:40.000000000 +0000
@@ -1,7 +1,10 @@
+import gc
+import time
from xml.dom import minidom
from django.core import serializers
-from django.core.serializers.xml_serializer import DTDForbidden
+from django.core.serializers.xml_serializer import Deserializer, DTDForbidden
+from django.db import models
from django.test import TestCase, TransactionTestCase
from .tests import SerializersTestBase, SerializersTransactionTestBase
@@ -90,6 +93,56 @@
with self.assertRaises(DTDForbidden):
next(serializers.deserialize("xml", xml))
+ def test_crafted_xml_performance(self):
+ """The time to process invalid inputs is not quadratic."""
+
+ def build_crafted_xml(depth, leaf_text_len):
+ nested_open = "" * depth
+ nested_close = "" * depth
+ leaf = "x" * leaf_text_len
+ field_content = f"{nested_open}{leaf}{nested_close}"
+ return f"""
+
+
+
+ """
+
+ def deserialize(crafted_xml):
+ iterator = Deserializer(crafted_xml)
+ gc.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)
+
class XmlSerializerTransactionTestCase(
SerializersTransactionTestBase, TransactionTestCase
diff -Nru python-django-4.2.23/tests/test_runner/test_parallel.py python-django-4.2.27/tests/test_runner/test_parallel.py
--- python-django-4.2.23/tests/test_runner/test_parallel.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/test_runner/test_parallel.py 2025-12-02 12:43:50.000000000 +0000
@@ -21,6 +21,12 @@
def __init__(self, arg):
super().__init__()
+ def __reduce__(self):
+ # tblib 3.2+ makes exception subclasses picklable by default.
+ # Return (cls, ()) so the constructor fails on unpickle, preserving
+ # the needed behavior for test_pickle_errors_detection.
+ return (self.__class__, ())
+
class ParallelTestRunnerTest(SimpleTestCase):
"""
@@ -102,6 +108,8 @@
result = RemoteTestResult()
result._confirm_picklable(picklable_error)
+ # The exception can be pickled but not unpickled.
+ pickle.dumps(not_unpicklable_error)
msg = "__init__() missing 1 required positional argument"
with self.assertRaisesMessage(TypeError, msg):
result._confirm_picklable(not_unpicklable_error)
diff -Nru python-django-4.2.23/tests/test_utils/tests.py python-django-4.2.27/tests/test_utils/tests.py
--- python-django-4.2.23/tests/test_utils/tests.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/test_utils/tests.py 2025-12-02 12:43:50.000000000 +0000
@@ -966,7 +966,7 @@
"('Unexpected end tag `div` (Line 1, Column 6)', (1, 6))"
)
with self.assertRaisesMessage(AssertionError, error_msg):
- self.assertHTMLEqual("< div> div>", "")
+ self.assertHTMLEqual("< div>", "")
with self.assertRaises(HTMLParseError):
parse_html("
")
diff -Nru python-django-4.2.23/tests/utils_tests/test_archive.py python-django-4.2.27/tests/utils_tests/test_archive.py
--- python-django-4.2.23/tests/utils_tests/test_archive.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/utils_tests/test_archive.py 2025-12-02 12:43:50.000000000 +0000
@@ -3,6 +3,7 @@
import sys
import tempfile
import unittest
+import zipfile
from django.core.exceptions import SuspiciousOperation
from django.test import SimpleTestCase
@@ -96,3 +97,21 @@
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
+ tarfile_handle.close()
+ self.addCleanup(os.remove, tar_path)
+
+ 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)
diff -Nru python-django-4.2.23/tests/utils_tests/test_html.py python-django-4.2.27/tests/utils_tests/test_html.py
--- python-django-4.2.23/tests/utils_tests/test_html.py 2025-06-10 09:51:07.000000000 +0000
+++ python-django-4.2.27/tests/utils_tests/test_html.py 2025-12-02 12:43:50.000000000 +0000
@@ -1,4 +1,5 @@
import os
+import sys
from datetime import datetime
from django.core.exceptions import SuspiciousOperation
@@ -85,6 +86,24 @@
self.check_output(linebreaks, lazystr(value), output)
def test_strip_tags(self):
+ # Python fixed a quadratic-time issue in HTMLParser in 3.13.6, 3.12.12,
+ # 3.11.14, 3.10.19, and 3.9.24. The fix slightly changes HTMLParser's
+ # output, so tests for particularly malformed input must handle both
+ # old and new results. The check below is temporary until all supported
+ # Python versions and CI workers include the fix. See:
+ # https://github.com/python/cpython/commit/6eb6c5db
+ min_fixed = {
+ (3, 14): (3, 14),
+ (3, 13): (3, 13, 6),
+ (3, 12): (3, 12, 12),
+ (3, 11): (3, 11, 14),
+ (3, 10): (3, 10, 19),
+ (3, 9): (3, 9, 24),
+ }
+ py_version = sys.version_info[:2]
+ htmlparser_fixed = (
+ py_version in min_fixed and sys.version_info >= min_fixed[py_version]
+ )
items = (
(
"See: 'é is an apostrophe followed by e acute
",
@@ -112,10 +131,16 @@
("&gotcha<>", "&gotcha<>"),
("ript>test</script>", "ript>test"),
("&h", "alert()h"),
- (">" if htmlparser_fixed else ">br>br>br>X", "XX"),
("<" * 50 + "a>" * 50, ""),
- (">" + "" + "" + "" if htmlparser_fixed else ">" + "