diff options
| author | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-06-03 09:23:37 -0400 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-06-03 12:12:54 -0400 |
| commit | 3328078a01f5268f9b8659f56fd28c5a2ed083dc (patch) | |
| tree | 4b473d337ae52c0de265911b4e2adcd11381231e | |
| parent | 170975c5bdc3fc69b15e46f50df7b48eb9e1115c (diff) | |
Refs CVE-2026-6873 -- Defaulted SIGNED_COOKIE_LEGACY_SALT_FALLBACK transitional setting to False.
| -rw-r--r-- | django/conf/__init__.py | 26 | ||||
| -rw-r--r-- | django/conf/global_settings.py | 2 | ||||
| -rw-r--r-- | django/core/signing.py | 3 | ||||
| -rw-r--r-- | docs/internals/deprecation.txt | 3 | ||||
| -rw-r--r-- | docs/ref/settings.txt | 15 | ||||
| -rw-r--r-- | docs/releases/6.1.txt | 13 | ||||
| -rw-r--r-- | tests/deprecation/test_signed_cookie_legacy_salt_fallback.py | 41 | ||||
| -rw-r--r-- | tests/signed_cookies_tests/tests.py | 13 |
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): |
