Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 18 additions & 23 deletions django/views/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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:
Expand Down
5 changes: 0 additions & 5 deletions docs/ref/request-response.txt
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,6 @@ Methods

See :doc:`cryptographic signing </topics/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
Expand Down
5 changes: 0 additions & 5 deletions docs/topics/cache.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
======================================

Expand Down
59 changes: 36 additions & 23 deletions tests/aggregation/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
15 changes: 15 additions & 0 deletions tests/view_tests/tests/test_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions tests/view_tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading