From 56050acb96abc7ac2079d83ffdd232056732bb47 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Wed, 17 Jun 2026 15:24:57 -0400 Subject: [PATCH 1/3] Fixed #37170 -- Applied the no-argument form of sensitive_post_parameters to MultiValueDict. --- django/views/debug.py | 41 ++++++++++++---------------- tests/view_tests/tests/test_debug.py | 15 ++++++++++ tests/view_tests/views.py | 18 ++++++++++++ 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/django/views/debug.py b/django/views/debug.py index caa3d05cd8a2..37dfe755a172 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -185,6 +185,17 @@ def is_active(self, request): """ return settings.DEBUG is False + def _cleanse_multivaluedict(self, multivaluedict, sensitive_post_parameters): + # The no-argument form of sensitive_post_parameters() marks every + # parameter as sensitive. + cleansed = multivaluedict.copy() + if sensitive_post_parameters == "__ALL__": + sensitive_post_parameters = list(multivaluedict) + for param in sensitive_post_parameters: + if param in multivaluedict: + cleansed[param] = self.cleansed_substitute + return cleansed + def get_cleansed_multivaluedict(self, request, multivaluedict): """ Replace the keys in a MultiValueDict marked as sensitive with stars. @@ -193,10 +204,9 @@ def get_cleansed_multivaluedict(self, request, multivaluedict): """ sensitive_post_parameters = getattr(request, "sensitive_post_parameters", []) if self.is_active(request) and sensitive_post_parameters: - multivaluedict = multivaluedict.copy() - for param in sensitive_post_parameters: - if param in multivaluedict: - multivaluedict[param] = self.cleansed_substitute + return self._cleanse_multivaluedict( + multivaluedict, sensitive_post_parameters + ) return multivaluedict def get_post_parameters(self, request): @@ -206,25 +216,10 @@ def get_post_parameters(self, request): """ if request is None: return {} - else: - sensitive_post_parameters = getattr( - request, "sensitive_post_parameters", [] - ) - if self.is_active(request) and sensitive_post_parameters: - cleansed = request.POST.copy() - if sensitive_post_parameters == "__ALL__": - # Cleanse all parameters. - for k in cleansed: - cleansed[k] = self.cleansed_substitute - return cleansed - else: - # Cleanse only the specified parameters. - for param in sensitive_post_parameters: - if param in cleansed: - cleansed[param] = self.cleansed_substitute - return cleansed - else: - return request.POST + sensitive_post_parameters = getattr(request, "sensitive_post_parameters", []) + if self.is_active(request) and sensitive_post_parameters: + return self._cleanse_multivaluedict(request.POST, sensitive_post_parameters) + return request.POST def cleanse_special_types(self, request, value): try: diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 229853b69019..c31d69619f20 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -50,6 +50,7 @@ multivalue_dict_key_error, non_sensitive_view, paranoid_view, + partially_sensitive_view, sensitive_args_function_caller, sensitive_kwargs_function_caller, sensitive_method_view, @@ -1673,6 +1674,20 @@ def test_paranoid_request(self): self.verify_paranoid_response(paranoid_view) self.verify_paranoid_email(paranoid_view) + def test_partially_sensitive_request(self): + """ + No POST parameters can be seen in the default error reports for views + decorated with the no-argument form of sensitive_post_parameters() + alongside a with-arguments form of sensitive_variables(). + """ + with self.settings(DEBUG=True): + self.verify_unsafe_response(partially_sensitive_view) + self.verify_unsafe_email(partially_sensitive_view) + + with self.settings(DEBUG=False): + self.verify_paranoid_response(partially_sensitive_view) + self.verify_paranoid_email(partially_sensitive_view) + def test_multivalue_dict_key_error(self): """ #21098 -- Sensitive POST parameters cannot be seen in the diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index 198634117796..0280e535ff7c 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -236,6 +236,24 @@ def paranoid_view(request): return technical_500_response(request, *exc_info) +@sensitive_variables("cooked_eggs", "sauce") +@sensitive_post_parameters() +def partially_sensitive_view(request): + # Do not just use plain strings for the variables' values in the code + # so that the tests don't return false positives when the function's source + # is displayed in the exception report. + cooked_eggs = "".join(["s", "c", "r", "a", "m", "b", "l", "e", "d"]) # NOQA + sauce = "".join( # NOQA + ["w", "o", "r", "c", "e", "s", "t", "e", "r", "s", "h", "i", "r", "e"] + ) + try: + raise request.POST["bar"] + except Exception: + exc_info = sys.exc_info() + send_log(request, exc_info) + return technical_500_response(request, *exc_info) + + def sensitive_args_function_caller(request): try: sensitive_args_function( From d01aaa5b1c8b89aedbcd9fbed497e4c69c72d0b1 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Tue, 23 Jun 2026 15:50:13 -0400 Subject: [PATCH 2/3] Removed additional versionchanged notices for 6.0. --- docs/ref/request-response.txt | 5 ----- docs/topics/cache.txt | 5 ----- 2 files changed, 10 deletions(-) diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 9ab3127df0b0..dcb6a8b057dd 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -448,11 +448,6 @@ Methods See :doc:`cryptographic signing ` for more information. - .. versionchanged:: 5.2.15 - - In older versions, cookies signed with distinct ``(key, salt)`` pairs - that concatenate to the same string could be used interchangeably. - .. method:: HttpRequest.is_secure() Returns ``True`` if the request is secure; that is, if it was made with diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index cc2459598ca8..d9f3009b0dc8 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -1429,11 +1429,6 @@ For more on Vary headers, see the :rfc:`official Vary spec responses, Django's ``CacheMiddleware`` adds ``Authorization`` to the ``Vary`` header to simplify construction of cache keys. -.. versionchanged:: 6.0.6 - - Previously, ``UpdateCacheMiddleware`` did not vary on ``Authorization`` for - requests bearing that header. - Controlling cache: Using other headers ====================================== From 420b4f5b0170d090d3b5b78b5c0d3986743e39db Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 23 Jun 2026 16:05:55 -0400 Subject: [PATCH 3/3] Relaxed StringAgg assertions to account for the lack of order_by. --- tests/aggregation/tests.py | 59 +++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index af46e616095b..eda328621714 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -2485,13 +2485,16 @@ def test_string_agg_escapes_delimiter(self): values = Publisher.objects.aggregate( stringagg=StringAgg("name", delimiter=Value("'")) ) - - self.assertEqual( - values, - { - "stringagg": "Apress'Sams'Prentice Hall'Morgan Kaufmann'Jonno's House " - "of Books", - }, + self.assertCountEqual( + values["stringagg"].split("'"), + [ + "Apress", + "Sams", + "Prentice Hall", + "Morgan Kaufmann", + "Jonno", + "s House of Books", + ], ) @skipUnlessDBFeature("supports_aggregate_order_by_clause") @@ -2546,13 +2549,15 @@ def test_string_agg_filter(self): filter=Q(name__startswith="P"), ) ) - - expected_values = { - "stringagg": "Practical Django Projects;" - "Python Web Development with Django;Paradigms of Artificial " - "Intelligence Programming: Case Studies in Common Lisp", - } - self.assertEqual(values, expected_values) + self.assertCountEqual( + values["stringagg"].split(";"), + [ + "Practical Django Projects", + "Python Web Development with Django", + "Paradigms of Artificial Intelligence Programming: Case " + "Studies in Common Lisp", + ], + ) @skipUnlessDBFeature("supports_aggregate_order_by_clause") def test_string_agg_filter_outerref(self): @@ -2623,16 +2628,24 @@ def test_string_agg_filter_in_subquery(self): ).values_list("agg", flat=True) ) - expected_values = [ - "Adrian Holovaty", - "Brad Dayley", - "Paul Bissex;Wesley J. Chun", - "Peter Norvig;Stuart Russell", - "Peter Norvig", - "" if connection.features.interprets_empty_strings_as_nulls else None, - ] + def normalize(v): + # Sort values for a normalized comparison since STRING_AGG() with + # ORDER BY isn't guaranteed to return results in a defined order. + if not v: # Don't sort None or "" + return v + return ";".join(sorted(v.split(";"))) - self.assertQuerySetEqual(expected_values, values, ordered=False) + self.assertCountEqual( + [normalize(v) for v in values], + [ + "Adrian Holovaty", + "Brad Dayley", + "Paul Bissex;Wesley J. Chun", + "Peter Norvig;Stuart Russell", + "Peter Norvig", + "" if connection.features.interprets_empty_strings_as_nulls else None, + ], + ) @skipUnlessDBFeature("supports_aggregate_order_by_clause") def test_order_by_in_subquery(self):