summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2026-06-03 09:23:37 -0400
committerJacob Walls <jacobtylerwalls@gmail.com>2026-06-03 12:12:54 -0400
commit3328078a01f5268f9b8659f56fd28c5a2ed083dc (patch)
tree4b473d337ae52c0de265911b4e2adcd11381231e
parent170975c5bdc3fc69b15e46f50df7b48eb9e1115c (diff)
Refs CVE-2026-6873 -- Defaulted SIGNED_COOKIE_LEGACY_SALT_FALLBACK transitional setting to False.
-rw-r--r--django/conf/__init__.py26
-rw-r--r--django/conf/global_settings.py2
-rw-r--r--django/core/signing.py3
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/ref/settings.txt15
-rw-r--r--docs/releases/6.1.txt13
-rw-r--r--tests/deprecation/test_signed_cookie_legacy_salt_fallback.py41
-rw-r--r--tests/signed_cookies_tests/tests.py13
8 files changed, 108 insertions, 8 deletions
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index 5bf4cf13ae..d462f82acf 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -26,6 +26,12 @@ DEFAULT_STORAGE_ALIAS = "default"
STATICFILES_STORAGE_ALIAS = "staticfiles"
# RemovedInDjango70Warning.
+SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG = (
+ "The SIGNED_COOKIE_LEGACY_SALT_FALLBACK transitional setting is "
+ "deprecated. Remove it from your settings once legacy signed cookies "
+ "have expired. They will not be accepted in Django 7.0."
+)
+# RemovedInDjango70Warning.
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG = (
"The USE_BLANK_CHOICE_DASH setting is deprecated. If you wish to define "
"your own default blank choice label, override "
@@ -149,6 +155,12 @@ class LazySettings(LazyObject):
self.__dict__.pop(name, None)
# RemovedInDjango70Warning.
+ if name == "SIGNED_COOKIE_LEGACY_SALT_FALLBACK":
+ _show_settings_deprecation_warning(
+ SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
+ RemovedInDjango70Warning,
+ )
+ # RemovedInDjango70Warning.
if name == "USE_BLANK_CHOICE_DASH":
_show_settings_deprecation_warning(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG, RemovedInDjango70Warning
@@ -260,6 +272,13 @@ class Settings:
self._explicit_settings.add(setting)
# RemovedInDjango70Warning.
+ if "SIGNED_COOKIE_LEGACY_SALT_FALLBACK" in self._explicit_settings:
+ warnings.warn(
+ SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
+ RemovedInDjango70Warning,
+ skip_file_prefixes=django_file_prefixes(),
+ )
+ # RemovedInDjango70Warning.
if "USE_BLANK_CHOICE_DASH" in self._explicit_settings:
warnings.warn(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG,
@@ -319,6 +338,13 @@ class UserSettingsHolder:
def __setattr__(self, name, value):
self._deleted.discard(name)
+ # RemovedInDjango70Warning.
+ if name == "SIGNED_COOKIE_LEGACY_SALT_FALLBACK":
+ _show_settings_deprecation_warning(
+ SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
+ RemovedInDjango70Warning,
+ )
+ # RemovedInDjango70Warning.
if name == "USE_BLANK_CHOICE_DASH":
_show_settings_deprecation_warning(
USE_BLANK_CHOICE_DASH_DEPRECATED_MSG, RemovedInDjango70Warning
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 12d76b05bb..a00c5c3922 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -561,7 +561,7 @@ AUTH_PASSWORD_VALIDATORS = []
# SIGNING #
###########
-SIGNED_COOKIE_LEGACY_SALT_FALLBACK = True
+SIGNED_COOKIE_LEGACY_SALT_FALLBACK = False
SIGNING_BACKEND = "django.core.signing.TimestampSigner"
########
diff --git a/django/core/signing.py b/django/core/signing.py
index 33f51d16aa..56b2c35a02 100644
--- a/django/core/signing.py
+++ b/django/core/signing.py
@@ -124,12 +124,15 @@ def _cookie_signer_salt(cookie_name, salt=""):
return f"django.http.cookies.v2:{len(salt)}:{salt}{cookie_name}"
+# RemovedInDjango70Warning: When the deprecation ends, remove.
def _cookie_signer_legacy_salt(cookie_name, salt=""):
return cookie_name + salt
def _unsign_cookie(signed_value, *, cookie_name, salt="", max_age=None):
try:
+ # RemovedInDjango70Warning: When the deprecation ends, replace the
+ # whole function body with this single return statement.
return get_cookie_signer(salt=_cookie_signer_salt(cookie_name, salt)).unsign(
signed_value, max_age=max_age
)
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 5a2aa5d5b8..4b586fef29 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -36,6 +36,9 @@ details on these changes.
* The ``URLIZE_ASSUME_HTTPS`` transitional setting will be removed.
+* The ``SIGNED_COOKIE_LEGACY_SALT_FALLBACK`` transitional setting will be
+ removed.
+
* Using a percent sign in a column alias or annotation will raise
``ValueError``.
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index b69cafad5a..31762338b9 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -2838,16 +2838,23 @@ See also :setting:`DATE_FORMAT` and :setting:`SHORT_DATE_FORMAT`.
.. versionadded:: 5.2.15
-Default: ``True``
+Default: ``False``
Controls whether :meth:`~django.http.HttpRequest.get_signed_cookie` accepts
cookies signed with Django's historical signed-cookie salt derivation based on
``key + salt``.
-Set this to ``False`` to reject those legacy signed cookies and only accept
+Set this to ``True`` to accept those legacy signed cookies in addition to
cookies signed with Django's current unambiguous signed-cookie salt derivation.
-This transitional setting will be removed in Django 7.0, when the legacy signed
-cookies will no longer be accepted.
+
+.. versionchanged:: 6.1
+
+ In older versions, the default was ``True``.
+
+.. deprecated:: 6.1
+
+ This transitional setting will be removed in Django 7.0, when legacy signed
+ cookies will no longer be accepted.
.. setting:: SIGNING_BACKEND
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index f9fb779ff3..7ef149f40c 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -331,6 +331,13 @@ Requests and Responses
the :func:`~django.shortcuts.redirect` shortcut, now accept a ``max_length``
parameter to override the default maximum URL length limit.
+Security
+~~~~~~~~
+
+* Signed cookies now use an unambiguous salt derivation by default. Set
+ :setting:`SIGNED_COOKIE_LEGACY_SALT_FALLBACK` to ``True`` to continue
+ accepting legacy signed cookies.
+
Serialization
~~~~~~~~~~~~~
@@ -508,6 +515,9 @@ Miscellaneous
* The minimum supported version of SQLite is increased from 3.31.0 to 3.37.0.
+* The default value of the transitional setting
+ :setting:`SIGNED_COOKIE_LEGACY_SALT_FALLBACK` is now ``False``.
+
* :class:`~django.contrib.contenttypes.fields.GenericForeignKey` now uses a
separate descriptor class: the private ``GenericForeignKeyDescriptor``.
@@ -625,6 +635,9 @@ Miscellaneous
* The :setting:`USE_BLANK_CHOICE_DASH` transitional setting is deprecated.
+* The :setting:`SIGNED_COOKIE_LEGACY_SALT_FALLBACK` transitional setting is
+ deprecated.
+
* The undocumented ``get_placeholder`` method of
:class:`~django.db.models.Field` is deprecated in favor of the newly
introduced ``get_placeholder_sql`` method, which has the same input signature
diff --git a/tests/deprecation/test_signed_cookie_legacy_salt_fallback.py b/tests/deprecation/test_signed_cookie_legacy_salt_fallback.py
new file mode 100644
index 0000000000..4b51707e45
--- /dev/null
+++ b/tests/deprecation/test_signed_cookie_legacy_salt_fallback.py
@@ -0,0 +1,41 @@
+import sys
+from types import ModuleType
+
+from django.conf import (
+ SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG,
+ LazySettings,
+ Settings,
+ settings,
+)
+from django.test import SimpleTestCase
+from django.utils.deprecation import RemovedInDjango70Warning
+
+
+# RemovedInDjango70Warning.
+class SignedCookieLegacySaltFallbackDeprecationTests(SimpleTestCase):
+ msg = SIGNED_COOKIE_LEGACY_SALT_DEPRECATED_MSG
+
+ def test_override_settings_warning(self):
+ with self.assertRaisesMessage(RemovedInDjango70Warning, self.msg):
+ with self.settings(SIGNED_COOKIE_LEGACY_SALT_FALLBACK=True):
+ pass
+
+ def test_settings_init_warning(self):
+ settings_module = ModuleType("fake_settings_module")
+ settings_module.USE_TZ = False
+ settings_module.SIGNED_COOKIE_LEGACY_SALT_FALLBACK = True
+ sys.modules["fake_settings_module"] = settings_module
+ try:
+ with self.assertRaisesMessage(RemovedInDjango70Warning, self.msg):
+ Settings("fake_settings_module")
+ finally:
+ del sys.modules["fake_settings_module"]
+
+ def test_settings_assignment_warning(self):
+ lazy_settings = LazySettings()
+ with self.assertRaisesMessage(RemovedInDjango70Warning, self.msg):
+ lazy_settings.SIGNED_COOKIE_LEGACY_SALT_FALLBACK = True
+
+ def test_access(self):
+ # Warning is not raised on access.
+ self.assertEqual(settings.SIGNED_COOKIE_LEGACY_SALT_FALLBACK, False)
diff --git a/tests/signed_cookies_tests/tests.py b/tests/signed_cookies_tests/tests.py
index 62bd3d192d..279da5ea59 100644
--- a/tests/signed_cookies_tests/tests.py
+++ b/tests/signed_cookies_tests/tests.py
@@ -3,10 +3,10 @@ from datetime import timedelta
from django.core import signing
from django.http import HttpRequest, HttpResponse
from django.test import SimpleTestCase, override_settings
-from django.test.utils import freeze_time
+from django.test.utils import freeze_time, ignore_warnings
+from django.utils.deprecation import RemovedInDjango70Warning
-@override_settings(SIGNED_COOKIE_LEGACY_SALT_FALLBACK=False)
class SignedCookieTest(SimpleTestCase):
def test_can_set_and_read_signed_cookies(self):
response = HttpResponse()
@@ -36,6 +36,8 @@ class SignedCookieTest(SimpleTestCase):
with self.assertRaises(signing.BadSignature):
request.get_signed_cookie("ab", salt="c")
+ # RemovedInDjango70Warning: When the deprecation ends, remove this test.
+ @ignore_warnings(category=RemovedInDjango70Warning)
@override_settings(SIGNED_COOKIE_LEGACY_SALT_FALLBACK=True)
def test_expired_legacy_cookie_raises_signature_expired(self):
with freeze_time(123456789):
@@ -47,8 +49,10 @@ class SignedCookieTest(SimpleTestCase):
with self.assertRaises(signing.SignatureExpired):
request.get_signed_cookie("a", salt="bc", max_age=10)
+ # RemovedInDjango70Warning: When the deprecation ends, remove this test.
+ @ignore_warnings(category=RemovedInDjango70Warning)
@override_settings(SIGNED_COOKIE_LEGACY_SALT_FALLBACK=True)
- def test_legacy_salt_namespace_is_accepted_by_default(self):
+ def test_legacy_salt_namespace_is_accepted(self):
request = HttpRequest()
# Simulate an attack along the lines of CVE-2026-6873, where a value
# for the "a" cookie is submitted as the value for another cookie.
@@ -58,6 +62,7 @@ class SignedCookieTest(SimpleTestCase):
# No protection since SIGNED_COOKIE_LEGACY_SALT_FALLBACK=True.
self.assertEqual(request.get_signed_cookie("ab", salt="c"), "hello")
+ # RemovedInDjango70Warning: When the deprecation ends, remove this test.
def test_legacy_salt_namespace_not_accepted(self):
request = HttpRequest()
request.COOKIES["a"] = signing.get_cookie_signer(
@@ -66,6 +71,8 @@ class SignedCookieTest(SimpleTestCase):
with self.assertRaises(signing.BadSignature):
request.get_signed_cookie("a", salt="bc")
+ # RemovedInDjango70Warning: When the deprecation ends, remove this test.
+ @ignore_warnings(category=RemovedInDjango70Warning)
@override_settings(SIGNED_COOKIE_LEGACY_SALT_FALLBACK=True)
def test_expired_new_style_cookie_does_not_fallback_to_legacy_salt(self):
with freeze_time(123456789):