From c880530ddd4fabd5939bab0e148bebe36699432a Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Thu, 16 Oct 2025 16:28:33 -0400 Subject: [PATCH 1/5] Fixed CVE-2025-64458 -- Mitigated potential DoS in HttpResponseRedirect/HttpResponsePermanentRedirect on Windows. Thanks Seokchan Yoon for the report, Markus Holtermann for the triage, and Jake Howard for the review. Follow-up to CVE-2025-27556 and 39e2297210d9d2938c75fc911d45f0e863dc4821. --- django/http/response.py | 9 +++++++-- docs/releases/4.2.26.txt | 10 +++++++++- docs/releases/5.1.14.txt | 10 +++++++++- docs/releases/5.2.8.txt | 10 ++++++++++ tests/httpwrappers/tests.py | 2 ++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/django/http/response.py b/django/http/response.py index 40b2d7089d55..020b2fcf3ab8 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -22,7 +22,7 @@ from django.utils.datastructures import CaseInsensitiveMapping from django.utils.encoding import iri_to_uri from django.utils.functional import cached_property -from django.utils.http import content_disposition_header, http_date +from django.utils.http import MAX_URL_LENGTH, content_disposition_header, http_date from django.utils.regex_helper import _lazy_re_compile _charset_from_content_type_re = _lazy_re_compile( @@ -631,7 +631,12 @@ class HttpResponseRedirectBase(HttpResponse): def __init__(self, redirect_to, preserve_request=False, *args, **kwargs): super().__init__(*args, **kwargs) self["Location"] = iri_to_uri(redirect_to) - parsed = urlsplit(str(redirect_to)) + redirect_to_str = str(redirect_to) + if len(redirect_to_str) > MAX_URL_LENGTH: + raise DisallowedRedirect( + f"Unsafe redirect exceeding {MAX_URL_LENGTH} characters" + ) + parsed = urlsplit(redirect_to_str) if preserve_request: self.status_code = self.status_code_preserve_request if parsed.scheme and parsed.scheme not in self.allowed_schemes: diff --git a/docs/releases/4.2.26.txt b/docs/releases/4.2.26.txt index e0db257c04d3..ae274c3361c0 100644 --- a/docs/releases/4.2.26.txt +++ b/docs/releases/4.2.26.txt @@ -7,4 +7,12 @@ Django 4.2.26 release notes Django 4.2.26 fixes one security issue with severity "high" and one security issue with severity "moderate" in 4.2.25. -... +CVE-2025-64458: Potential denial-of-service vulnerability in ``HttpResponseRedirect`` and ``HttpResponsePermanentRedirect`` on Windows +====================================================================================================================================== + +Python's :func:`NFKC normalization ` is slow on +Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`, +:class:`~django.http.HttpResponsePermanentRedirect`, and the shortcut +:func:`redirect() ` were subject to a potential +denial-of-service attack via certain inputs with a very large number of Unicode +characters (follow up to :cve:`2025-27556`). diff --git a/docs/releases/5.1.14.txt b/docs/releases/5.1.14.txt index 79a7a260e302..8dba96e48726 100644 --- a/docs/releases/5.1.14.txt +++ b/docs/releases/5.1.14.txt @@ -7,4 +7,12 @@ Django 5.1.14 release notes Django 5.1.14 fixes one security issue with severity "high" and one security issue with severity "moderate" in 5.1.13. -... +CVE-2025-64458: Potential denial-of-service vulnerability in ``HttpResponseRedirect`` and ``HttpResponsePermanentRedirect`` on Windows +====================================================================================================================================== + +Python's :func:`NFKC normalization ` is slow on +Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`, +:class:`~django.http.HttpResponsePermanentRedirect`, and the shortcut +:func:`redirect() ` were subject to a potential +denial-of-service attack via certain inputs with a very large number of Unicode +characters (follow up to :cve:`2025-27556`). diff --git a/docs/releases/5.2.8.txt b/docs/releases/5.2.8.txt index 62cb32f55c29..947fce8d84de 100644 --- a/docs/releases/5.2.8.txt +++ b/docs/releases/5.2.8.txt @@ -8,6 +8,16 @@ Django 5.2.8 fixes one security issue with severity "high", one security issue with severity "moderate", and several bugs in 5.2.7. It also adds compatibility with Python 3.14. +CVE-2025-64458: Potential denial-of-service vulnerability in ``HttpResponseRedirect`` and ``HttpResponsePermanentRedirect`` on Windows +====================================================================================================================================== + +Python's :func:`NFKC normalization ` is slow on +Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`, +:class:`~django.http.HttpResponsePermanentRedirect`, and the shortcut +:func:`redirect() ` were subject to a potential +denial-of-service attack via certain inputs with a very large number of Unicode +characters (follow up to :cve:`2025-27556`). + Bugfixes ======== diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index f1caec6b710f..804bec50b08b 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -24,6 +24,7 @@ ) from django.test import SimpleTestCase from django.utils.functional import lazystr +from django.utils.http import MAX_URL_LENGTH class QueryDictTests(SimpleTestCase): @@ -490,6 +491,7 @@ def test_unsafe_redirect(self): 'data:text/html,', "mailto:test@example.com", "file:///etc/passwd", + "é" * (MAX_URL_LENGTH + 1), ] for url in bad_urls: with self.assertRaises(DisallowedRedirect): From 98e642c69181c942d60a10ca0085d48c6b3068bb Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 24 Sep 2025 15:54:51 -0400 Subject: [PATCH 2/5] Fixed CVE-2025-64459 -- Prevented SQL injections in Q/QuerySet via the _connector kwarg. Thanks cyberstan for the report, Sarah Boyce, Adam Johnson, Simon Charette, and Jake Howard for the reviews. --- django/db/models/query_utils.py | 4 ++++ docs/releases/4.2.26.txt | 7 +++++++ docs/releases/5.1.14.txt | 7 +++++++ docs/releases/5.2.8.txt | 7 +++++++ tests/queries/test_q.py | 5 +++++ 5 files changed, 30 insertions(+) diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 23d543211abc..8920977cd270 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -48,8 +48,12 @@ class Q(tree.Node): XOR = "XOR" default = AND conditional = True + connectors = (None, AND, OR, XOR) def __init__(self, *args, _connector=None, _negated=False, **kwargs): + if _connector not in self.connectors: + connector_reprs = ", ".join(f"{conn!r}" for conn in self.connectors[1:]) + raise ValueError(f"_connector must be one of {connector_reprs}, or None.") super().__init__( children=[*args, *sorted(kwargs.items())], connector=_connector, diff --git a/docs/releases/4.2.26.txt b/docs/releases/4.2.26.txt index ae274c3361c0..20cf48f05c11 100644 --- a/docs/releases/4.2.26.txt +++ b/docs/releases/4.2.26.txt @@ -16,3 +16,10 @@ Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`, :func:`redirect() ` were subject to a potential denial-of-service attack via certain inputs with a very large number of Unicode characters (follow up to :cve:`2025-27556`). + +CVE-2025-64459: Potential SQL injection via ``_connector`` keyword argument +=========================================================================== + +:meth:`.QuerySet.filter`, :meth:`~.QuerySet.exclude`, :meth:`~.QuerySet.get`, +and :class:`~.Q` were subject to SQL injection using a suitably crafted +dictionary, with dictionary expansion, as the ``_connector`` argument. diff --git a/docs/releases/5.1.14.txt b/docs/releases/5.1.14.txt index 8dba96e48726..d762dbf3cd27 100644 --- a/docs/releases/5.1.14.txt +++ b/docs/releases/5.1.14.txt @@ -16,3 +16,10 @@ Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`, :func:`redirect() ` were subject to a potential denial-of-service attack via certain inputs with a very large number of Unicode characters (follow up to :cve:`2025-27556`). + +CVE-2025-64459: Potential SQL injection via ``_connector`` keyword argument +=========================================================================== + +:meth:`.QuerySet.filter`, :meth:`~.QuerySet.exclude`, :meth:`~.QuerySet.get`, +and :class:`~.Q` were subject to SQL injection using a suitably crafted +dictionary, with dictionary expansion, as the ``_connector`` argument. diff --git a/docs/releases/5.2.8.txt b/docs/releases/5.2.8.txt index 947fce8d84de..0a0038ba2096 100644 --- a/docs/releases/5.2.8.txt +++ b/docs/releases/5.2.8.txt @@ -18,6 +18,13 @@ Windows. As a consequence, :class:`~django.http.HttpResponseRedirect`, 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. + Bugfixes ======== diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py index 1a62aca06114..52200b2ecff1 100644 --- a/tests/queries/test_q.py +++ b/tests/queries/test_q.py @@ -272,6 +272,11 @@ def test_create_helper(self): Q(*items, _connector=connector), ) + def test_connector_validation(self): + msg = f"_connector must be one of {Q.AND!r}, {Q.OR!r}, {Q.XOR!r}, or None." + with self.assertRaisesMessage(ValueError, msg): + Q(_connector="evil") + def test_referenced_base_fields(self): # Make sure Q.referenced_base_fields retrieves all base fields from # both filters and F expressions. From 3c3f46357718166069948625354b8315a8505262 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 24 Sep 2025 15:56:03 -0400 Subject: [PATCH 3/5] Refs CVE-2025-64459 -- Avoided propagating invalid arguments to Q on dictionary expansion. --- django/db/models/query.py | 5 +++++ tests/queries/tests.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/django/db/models/query.py b/django/db/models/query.py index 70177667a606..3c9c42cdc774 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -46,6 +46,8 @@ # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 +PROHIBITED_FILTER_KWARGS = frozenset(["_connector", "_negated"]) + class BaseIterable: def __init__( @@ -1645,6 +1647,9 @@ def _filter_or_exclude(self, negate, args, kwargs): return clone def _filter_or_exclude_inplace(self, negate, args, kwargs): + if invalid_kwargs := PROHIBITED_FILTER_KWARGS.intersection(kwargs): + invalid_kwargs_str = ", ".join(f"'{k}'" for k in sorted(invalid_kwargs)) + raise TypeError(f"The following kwargs are invalid: {invalid_kwargs_str}") if negate: self._query.add_q(~Q(*args, **kwargs)) else: diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 4ee457271904..51d1915c97c5 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -4516,6 +4516,14 @@ def test_invalid_values(self): 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): """ From 6e18c078d5c044a1d22dd6a64ace11d9e5f6d0bc Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:12:30 -0300 Subject: [PATCH 4/5] Added stub release notes for 5.2.9. --- docs/releases/5.2.9.txt | 12 ++++++++++++ docs/releases/index.txt | 1 + 2 files changed, 13 insertions(+) create mode 100644 docs/releases/5.2.9.txt diff --git a/docs/releases/5.2.9.txt b/docs/releases/5.2.9.txt new file mode 100644 index 000000000000..5698bdc20643 --- /dev/null +++ b/docs/releases/5.2.9.txt @@ -0,0 +1,12 @@ +========================== +Django 5.2.9 release notes +========================== + +*Expected December 2, 2025* + +Django 5.2.9 fixes several bugs in 5.2.8. + +Bugfixes +======== + +* ... diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 18605ac878d2..38edcf79adcf 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -39,6 +39,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 5.2.9 5.2.8 5.2.7 5.2.6 From c5a107e8248813f07325ae65232b5e53e9ac4238 Mon Sep 17 00:00:00 2001 From: Natalia <124304+nessita@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:17:12 -0300 Subject: [PATCH 5/5] Added CVE-2025-64458 and CVE-2025-64459 to security archive. --- docs/releases/security.txt | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/releases/security.txt b/docs/releases/security.txt index 2bdb75e0cce1..2c6418afd06f 100644 --- a/docs/releases/security.txt +++ b/docs/releases/security.txt @@ -36,6 +36,30 @@ 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 +`__ + +* Django 6.0 :commit:`(patch) <6e13348436fccf8f22982921d6a3a3e65c956a9f>` +* Django 5.2 :commit:`(patch) <4f5d904b63751dea9ffc3b0e046404a7fa5881ac>` +* Django 5.1 :commit:`(patch) <3790593781d26168e7306b5b2f8ea0309de16242>` +* Django 4.2 :commit:`(patch) <770eea38d7a0e9ba9455140b5a9a9e33618226a7>` + +November 5, 2025 - :cve:`2025-64459` +------------------------------------ + +Potential SQL injection via ``_connector`` keyword argument in ``QuerySet`` and +``Q`` objects. `Full description +`__ + +* Django 6.0 :commit:`(patch) <06dd38324ac3d60d83d9f3adabf0dcdf423d2a85>` +* Django 5.2 :commit:`(patch) <6703f364d767e949c5b0e4016433ef75063b4f9b>` +* Django 5.1 :commit:`(patch) <72d2c87431f2ae0431d65d0ec792047f078c8241>` +* Django 4.2 :commit:`(patch) <59ae82e67053d281ff4562a24bbba21299f0a7d4>` + October 1, 2025 - :cve:`2025-59681` -----------------------------------