Package: release.debian.org
Severity: normal
Tags: trixie
User: [email protected]
Usertags: pu

Dear stable release managers,

Re. https://salsa.debian.org/lts-team/lts-updates-tasks/-/issues/300,
please consider python-django (3:4.2.27-1+deb13u1) for trixie:
  
  python-django (3:4.2.27-1+deb13u1) trixie; 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)
  .
      <https://docs.djangoproject.com/en/4.2/releases/4.2.27/>


The relevant Debusine job is as follows:

  https://debusine.debian.net/debian/developers/work-request/357875/

There are three failures of autopkgtests in reverse-dependencies, which I
have investigated as follows:

  * src:debusine — appears to be some unshare(1)/namespace issue.
  * src:django-ldapdb — "Depends: python3-volatildap but it is not installable"
  * src:pyinstaller —  Socket/internet issue 
"tests/functional/test_scipy.py::test_scipy[onedir-scipy.spatial] Error: 
websocket: close 1006 (abnormal closure): unexpected EOF"

The full diff is attached.


Regards,

-- 
      ,''`.
     : :'  :     Chris Lamb
     `. `'`      [email protected] / chris-lamb.co.uk
       `-
diff --git debian/changelog debian/changelog
index 8120d436f..a4c0b0d99 100644
--- debian/changelog
+++ debian/changelog
@@ -1,3 +1,52 @@
+python-django (3:4.2.27-1+deb13u1) trixie; 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)
+
+    <https://docs.djangoproject.com/en/4.2/releases/4.2.27/>
+
+ -- Chris Lamb <[email protected]>  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 --git Django.egg-info/PKG-INFO Django.egg-info/PKG-INFO
index 5c98d60ee..fe5c1526a 100644
--- Django.egg-info/PKG-INFO
+++ Django.egg-info/PKG-INFO
@@ -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 <[email protected]>
 License: BSD-3-Clause
diff --git Django.egg-info/SOURCES.txt Django.egg-info/SOURCES.txt
index ca8895c96..c03bfd953 100644
--- Django.egg-info/SOURCES.txt
+++ Django.egg-info/SOURCES.txt
@@ -3315,6 +3315,7 @@ django/db/backends/oracle/validation.py
 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.20.txt
 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/inlines.test.js
 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 --git MANIFEST.in MANIFEST.in
index cba764b41..0492e7cbc 100644
--- MANIFEST.in
+++ MANIFEST.in
@@ -10,6 +10,6 @@ graft django
 graft docs
 graft extras
 graft js_tests
-graft scripts
 graft tests
 global-exclude *.py[co]
+prune scripts
diff --git PKG-INFO PKG-INFO
index 5c98d60ee..fe5c1526a 100644
--- PKG-INFO
+++ PKG-INFO
@@ -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 <[email protected]>
 License: BSD-3-Clause
diff --git django/__init__.py django/__init__.py
index 49e1e4201..06680666d 100644
--- django/__init__.py
+++ django/__init__.py
@@ -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 --git django/core/serializers/xml_serializer.py 
django/core/serializers/xml_serializer.py
index 3f9955aa2..5db8c067c 100644
--- django/core/serializers/xml_serializer.py
+++ django/core/serializers/xml_serializer.py
@@ -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.db import DEFAULT_DB_ALIAS, models
 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."""
 
@@ -208,7 +228,8 @@ class Deserializer(base.Deserializer):
     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 @@ class Deserializer(base.Deserializer):
 
 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 django/db/backends/postgresql/compiler.py 
django/db/backends/postgresql/compiler.py
new file mode 100644
index 000000000..d4140c7f9
--- /dev/null
+++ django/db/backends/postgresql/compiler.py
@@ -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 --git django/db/backends/postgresql/operations.py 
django/db/backends/postgresql/operations.py
index c4d90b56a..24ad90e5f 100644
--- django/db/backends/postgresql/operations.py
+++ django/db/backends/postgresql/operations.py
@@ -23,6 +23,7 @@ def get_json_dumps(encoder):
 
 
 class DatabaseOperations(BaseDatabaseOperations):
+    compiler_module = "django.db.backends.postgresql.compiler"
     cast_char_field_without_max_length = "varchar"
     explain_prefix = "EXPLAIN"
     explain_options = frozenset(
diff --git django/db/models/query.py django/db/models/query.py
index 19c9ced23..31718a3a3 100644
--- django/db/models/query.py
+++ 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__(
@@ -1455,6 +1457,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 django/db/models/query_utils.py django/db/models/query_utils.py
index 5c5644cfb..a85a682e5 100644
--- django/db/models/query_utils.py
+++ django/db/models/query_utils.py
@@ -44,8 +44,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 django/db/models/sql/query.py django/db/models/sql/query.py
index e68fd9efb..3b8071eab 100644
--- django/db/models/sql/query.py
+++ django/db/models/sql/query.py
@@ -46,9 +46,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
@@ -1123,8 +1123,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):
@@ -1620,6 +1620,7 @@ class Query(BaseExpression):
         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 --git django/http/response.py django/http/response.py
index ea3114170..28b8fa6d3 100644
--- django/http/response.py
+++ django/http/response.py
@@ -21,7 +21,11 @@ from django.http.cookie import SimpleCookie
 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 @@ class HttpResponseRedirectBase(HttpResponse):
     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 --git django/utils/archive.py django/utils/archive.py
index 71ec2d001..e8af690e2 100644
--- django/utils/archive.py
+++ django/utils/archive.py
@@ -144,7 +144,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 django/utils/html.py django/utils/html.py
index 84c37d118..11ffd53eb 100644
--- django/utils/html.py
+++ django/utils/html.py
@@ -9,12 +9,11 @@ from urllib.parse import parse_qsl, quote, unquote, 
urlencode, urlsplit, urlunsp
 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 --git django/utils/http.py django/utils/http.py
index 94ad60bdb..0804937f3 100644
--- django/utils/http.py
+++ django/utils/http.py
@@ -47,6 +47,8 @@ ASCTIME_DATE = _lazy_re_compile(r"^\w{3} %s %s %s %s$" % 
(__M, __D2, __T, __Y))
 
 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 --git docs/internals/contributing/writing-code/submitting-patches.txt 
docs/internals/contributing/writing-code/submitting-patches.txt
index be031f1f6..0a9d6b0d1 100644
--- docs/internals/contributing/writing-code/submitting-patches.txt
+++ docs/internals/contributing/writing-code/submitting-patches.txt
@@ -320,8 +320,8 @@ All code changes
 
 * Does the :doc:`coding style
   </internals/contributing/writing-code/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
   <coding-style-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 --git docs/internals/contributing/writing-code/unit-tests.txt 
docs/internals/contributing/writing-code/unit-tests.txt
index 874038a55..d9f8a276e 100644
--- docs/internals/contributing/writing-code/unit-tests.txt
+++ docs/internals/contributing/writing-code/unit-tests.txt
@@ -69,7 +69,7 @@ command from any place in the Django source tree:
     $ 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 @@ can be seen as follows:
     flake8>=3.7.0
     docs
     isort>=5.1.0
+    zizmor>=1.16.3
 
 Testing other Python versions and database backends
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -284,7 +285,7 @@ dependencies:
 * :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 --git docs/ref/contrib/admin/admindocs.txt 
docs/ref/contrib/admin/admindocs.txt
index cc121a7be..9806aa243 100644
--- docs/ref/contrib/admin/admindocs.txt
+++ docs/ref/contrib/admin/admindocs.txt
@@ -23,7 +23,8 @@ the following:
   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 --git docs/releases/4.2.24.txt docs/releases/4.2.24.txt
new file mode 100644
index 000000000..e50148391
--- /dev/null
+++ docs/releases/4.2.24.txt
@@ -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 --git docs/releases/4.2.25.txt docs/releases/4.2.25.txt
new file mode 100644
index 000000000..7ba23c013
--- /dev/null
+++ docs/releases/4.2.25.txt
@@ -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 --git docs/releases/4.2.26.txt docs/releases/4.2.26.txt
new file mode 100644
index 000000000..20cf48f05
--- /dev/null
+++ docs/releases/4.2.26.txt
@@ -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 <python:unicodedata.normalize>` is slow on
+Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`,
+:class:`~django.http.HttpResponsePermanentRedirect`, and the shortcut
+:func:`redirect() <django.shortcuts.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 --git docs/releases/4.2.27.txt docs/releases/4.2.27.txt
new file mode 100644
index 000000000..b843f6a44
--- /dev/null
+++ docs/releases/4.2.27.txt
@@ -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 <serialization-formats-xml>` 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 --git docs/releases/index.txt docs/releases/index.txt
index 73195b535..f5a6770bc 100644
--- docs/releases/index.txt
+++ docs/releases/index.txt
@@ -26,6 +26,10 @@ versions of the documentation contain the release notes for 
any later releases.
 .. 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 --git docs/releases/security.txt docs/releases/security.txt
index de8fc96d6..e5b9878ab 100644
--- docs/releases/security.txt
+++ docs/releases/security.txt
@@ -36,6 +36,65 @@ Issues under Django's security process
 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
+<https://www.djangoproject.com/weblog/2025/nov/05/security-releases/>`__
+
+* 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
+<https://www.djangoproject.com/weblog/2025/nov/05/security-releases/>`__
+
+* 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
+<https://www.djangoproject.com/weblog/2025/oct/01/security-releases/>`__
+
+* 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
+<https://www.djangoproject.com/weblog/2025/oct/01/security-releases/>`__
+
+* Django 6.0 :commit:`(patch) <af067f56c1dd467df4abd0ddd409a700da1f03ba>`
+* Django 5.2 :commit:`(patch) <ed8fc39d77465eddbde1191a054ae965f6a8a584>`
+* 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
+<https://www.djangoproject.com/weblog/2025/sep/03/security-releases/>`__
+
+* 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 @@ Potential log injection via unescaped request path.
 * Django 5.1 :commit:`(patch) <596542ddb46cdabe011322917e1655f0d24eece2>`
 * Django 4.2 :commit:`(patch) <ac03c5e7df8680c61cdb0d3bdb8be9095dba841e>`
 
+There was an additional hardening with new patch releases published on June 10,
+2025. `Full description
+<https://www.djangoproject.com/weblog/2025/jun/10/bugfix-releases/>`__
+
+* Django 5.2.3 :commit:`(patch) <8fcc83953c350e158a484bf1da0aa1b79b69bb07>`
+* Django 5.1.11 :commit:`(patch) <31f4bd31fa16f7f5302f65b9b8b7a49b69a7c4a6>`
+* Django 4.2.23 :commit:`(patch) <b597d46bb19c8567615e62029210dab16c70db7d>`
+
 May 7, 2025 - :cve:`2025-32873`
 -------------------------------
 
diff --git docs/topics/serialization.txt docs/topics/serialization.txt
index 0bb57642a..dc403ca1d 100644
--- docs/topics/serialization.txt
+++ docs/topics/serialization.txt
@@ -173,6 +173,8 @@ Identifier  Information
 .. _jsonl: https://jsonlines.org/
 .. _PyYAML: https://pyyaml.org/
 
+.. _serialization-formats-xml:
+
 XML
 ---
 
diff --git scripts/manage_translations.py scripts/manage_translations.py
deleted file mode 100644
index 5b82011f2..000000000
--- scripts/manage_translations.py
+++ /dev/null
@@ -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 --git tests/aggregation/tests.py tests/aggregation/tests.py
index 48266d977..277c0507f 100644
--- tests/aggregation/tests.py
+++ tests/aggregation/tests.py
@@ -2090,8 +2090,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 tests/annotations/tests.py tests/annotations/tests.py
index e0cdbf1e0..d876e3a6f 100644
--- tests/annotations/tests.py
+++ tests/annotations/tests.py
@@ -2,6 +2,7 @@ import datetime
 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 @@ from django.db.models import (
     Exists,
     ExpressionWrapper,
     F,
+    FilteredRelation,
     FloatField,
     Func,
     IntegerField,
@@ -1115,12 +1117,21 @@ 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)})
 
+    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 @@ 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):
                 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 @@ 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)})
+
+    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 --git tests/expressions/test_queryset_values.py 
tests/expressions/test_queryset_values.py
index 47bd1358d..080ee0618 100644
--- tests/expressions/test_queryset_values.py
+++ 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 tests/gis_tests/gdal_tests/test_raster.py 
tests/gis_tests/gdal_tests/test_raster.py
index cbe7d6b36..e1c135315 100644
--- tests/gis_tests/gdal_tests/test_raster.py
+++ tests/gis_tests/gdal_tests/test_raster.py
@@ -6,7 +6,7 @@ import zipfile
 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 @@ class GDALRasterTests(SimpleTestCase):
         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 --git tests/gis_tests/relatedapp/tests.py 
tests/gis_tests/relatedapp/tests.py
index e11a410af..74aa64c0a 100644
--- tests/gis_tests/relatedapp/tests.py
+++ tests/gis_tests/relatedapp/tests.py
@@ -98,10 +98,15 @@ class RelatedGeoModelTest(TestCase):
         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 --git tests/httpwrappers/tests.py tests/httpwrappers/tests.py
index fa2c8fd5d..3af20f930 100644
--- tests/httpwrappers/tests.py
+++ tests/httpwrappers/tests.py
@@ -24,6 +24,7 @@ from django.http import (
 )
 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 @@ class HttpResponseTests(SimpleTestCase):
         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,<script>window.alert("xss")</script>',
             "mailto:[email protected]";,
             "file:///etc/passwd",
+            "é" * (MAX_URL_REDIRECT_LENGTH + 1),
         ]
         for url in bad_urls:
             with self.assertRaises(DisallowedRedirect):
diff --git tests/queries/test_q.py tests/queries/test_q.py
index cdf40292b..5f20a4176 100644
--- tests/queries/test_q.py
+++ tests/queries/test_q.py
@@ -225,6 +225,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")
+
 
 class QCheckTests(TestCase):
     def test_basic(self):
diff --git tests/queries/tests.py tests/queries/tests.py
index a6a2b252e..2290ea29b 100644
--- tests/queries/tests.py
+++ tests/queries/tests.py
@@ -1943,8 +1943,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"})
@@ -4490,6 +4490,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):
         """
diff --git tests/requirements/py3.txt tests/requirements/py3.txt
index 7eebc20e0..b166df7bb 100644
--- tests/requirements/py3.txt
+++ tests/requirements/py3.txt
@@ -4,7 +4,7 @@ argon2-cffi >= 19.2.0
 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 --git tests/serializers/test_xml.py tests/serializers/test_xml.py
index c9df2f2a5..03462cfed 100644
--- tests/serializers/test_xml.py
+++ tests/serializers/test_xml.py
@@ -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 @@ class XmlSerializerTestCase(SerializersTestBase, TestCase):
         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 = "<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 = 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 --git tests/test_runner/test_parallel.py tests/test_runner/test_parallel.py
index eea9e4de7..5fbd0658b 100644
--- tests/test_runner/test_parallel.py
+++ tests/test_runner/test_parallel.py
@@ -21,6 +21,12 @@ class ExceptionThatFailsUnpickling(Exception):
     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 @@ class RemoteTestResultTest(SimpleTestCase):
         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 --git tests/test_utils/tests.py tests/test_utils/tests.py
index a440e7d96..e4c613e96 100644
--- tests/test_utils/tests.py
+++ tests/test_utils/tests.py
@@ -966,7 +966,7 @@ class HTMLEqualTests(SimpleTestCase):
             "('Unexpected end tag `div` (Line 1, Column 6)', (1, 6))"
         )
         with self.assertRaisesMessage(AssertionError, error_msg):
-            self.assertHTMLEqual("< div></ div>", "<div></div>")
+            self.assertHTMLEqual("< div></div>", "<div></div>")
         with self.assertRaises(HTMLParseError):
             parse_html("</p>")
 
diff --git tests/utils_tests/test_archive.py tests/utils_tests/test_archive.py
index 8cd107063..8063dafb6 100644
--- tests/utils_tests/test_archive.py
+++ 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
@@ -96,3 +97,21 @@ 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
+            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 --git tests/utils_tests/test_html.py tests/utils_tests/test_html.py
index 25168e234..f755b8ceb 100644
--- tests/utils_tests/test_html.py
+++ tests/utils_tests/test_html.py
@@ -1,4 +1,5 @@
 import os
+import sys
 from datetime import datetime
 
 from django.core.exceptions import SuspiciousOperation
@@ -85,6 +86,24 @@ class TestUtilsHtml(SimpleTestCase):
                 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 = (
             (
                 "<p>See: &#39;&eacute; is an apostrophe followed by e 
acute</p>",
@@ -112,10 +131,16 @@ 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 else "><!" + ("&" * 16000) + "D",
+            ),
             ("X<<<<br>br>br>br>X", "XX"),
             ("<" * 50 + "a>" * 50, ""),
-            (">" + "<a" * 500 + "a", ">" + "<a" * 500 + "a"),
+            (
+                ">" + "<a" * 500 + "a",
+                ">" if htmlparser_fixed else ">" + "<a" * 500 + "a",
+            ),
             ("<a" * 49 + "a" * 951, "<a" * 49 + "a" * 951),
             ("<" + "a" * 1_002, "<" + "a" * 1_002),
         )

Reply via email to