Your message dated Sat, 31 Jan 2026 22:12:28 +0000
with message-id 
<49f949862e48ee42e5524d0bf074ebe885eb8ac7.ca...@adam-barratt.org.uk>
and subject line Re: Bug#1126461: trixie-pu: package 
python-django/3:4.2.27-1+deb13u1
has caused the Debian Bug report #1126461,
regarding trixie-pu: package python-django/3:4.2.27-1+deb13u1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1126461: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1126461
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
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),
         )

--- End Message ---
--- Begin Message ---
On Wed, 2026-01-28 at 07:23 +0100, Salvatore Bonaccorso wrote:
> Hi,
> 
> On Tue, Jan 27, 2026 at 08:17:55AM +0100, Moritz Mühlenhoff wrote:
> > Am Mon, Jan 26, 2026 at 11:21:46AM -0800 schrieb Chris Lamb:
> > > 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
> > 
> > Let's fix Django via a DSA instead, I'll review the debdiff in the
> > coming
> > days and get back to you.
> 
> IMHO though the version has to be choosen differently. Is this a new
> usptream version inport on top of the packaging?
> 
> Then please choose 3:4.2.27-0+deb13u1 instead. This is even more
> importantly to do as there was a 3:4.2.27-1 upload.

That happened as DSA 6117-1. Closing the p-u bug.

Regards,

Adam

--- End Message ---

Reply via email to