Skip to content

Sanitize sensitive variables in RequestPanel #2105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
20 changes: 6 additions & 14 deletions debug_toolbar/panels/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.utils.translation import gettext_lazy as _

from debug_toolbar.panels import Panel
from debug_toolbar.utils import get_name_from_obj, get_sorted_request_variable
from debug_toolbar.utils import get_name_from_obj, sanitize_and_sort_request_vars


class RequestPanel(Panel):
Expand All @@ -26,9 +26,9 @@ def nav_subtitle(self):
def generate_stats(self, request, response):
self.record_stats(
{
"get": get_sorted_request_variable(request.GET),
"post": get_sorted_request_variable(request.POST),
"cookies": get_sorted_request_variable(request.COOKIES),
"get": sanitize_and_sort_request_vars(request.GET),
"post": sanitize_and_sort_request_vars(request.POST),
"cookies": sanitize_and_sort_request_vars(request.COOKIES),
}
)

Expand Down Expand Up @@ -59,13 +59,5 @@ def generate_stats(self, request, response):
self.record_stats(view_info)

if hasattr(request, "session"):
try:
session_list = [
(k, request.session.get(k)) for k in sorted(request.session.keys())
]
except TypeError:
session_list = [
(k, request.session.get(k))
for k in request.session.keys() # (it's not a dict)
]
self.record_stats({"session": {"list": session_list}})
session_data = dict(request.session)
self.record_stats({"session": sanitize_and_sort_request_vars(session_data)})
46 changes: 39 additions & 7 deletions debug_toolbar/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
from django.template import Node
from django.utils.html import format_html
from django.utils.safestring import SafeString, mark_safe
from django.views.debug import get_default_exception_reporter_filter

from debug_toolbar import _stubs as stubs, settings as dt_settings

_local_data = Local()
safe_filter = get_default_exception_reporter_filter()


def _is_excluded_frame(frame: Any, excluded_modules: Sequence[str] | None) -> bool:
Expand Down Expand Up @@ -215,20 +217,50 @@ def getframeinfo(frame: Any, context: int = 1) -> inspect.Traceback:
return inspect.Traceback(filename, lineno, frame.f_code.co_name, lines, index)


def get_sorted_request_variable(
def sanitize_and_sort_request_vars(
variable: dict[str, Any] | QueryDict,
) -> dict[str, list[tuple[str, Any]] | Any]:
"""
Get a data structure for showing a sorted list of variables from the
request data.
request data with sensitive values redacted.
"""
if not isinstance(variable, (dict, QueryDict)):
return {"raw": variable}

# Get sorted keys if possible, otherwise just list them
keys = _get_sorted_keys(variable)

# Process the variable based on its type
if isinstance(variable, QueryDict):
result = _process_query_dict(variable, keys)
else:
result = _process_dict(variable, keys)

return {"list": result}


def _get_sorted_keys(variable):
"""Helper function to get sorted keys if possible."""
try:
if isinstance(variable, dict):
return {"list": [(k, variable.get(k)) for k in sorted(variable)]}
else:
return {"list": [(k, variable.getlist(k)) for k in sorted(variable)]}
return sorted(variable)
except TypeError:
return {"raw": variable}
return list(variable)


def _process_query_dict(query_dict, keys):
"""Process a QueryDict into a list of (key, sanitized_value) tuples."""
result = []
for k in keys:
values = query_dict.getlist(k)
# Return single value if there's only one, otherwise keep as list
value = values[0] if len(values) == 1 else values
result.append((k, safe_filter.cleanse_setting(k, value)))
return result


def _process_dict(dictionary, keys):
"""Process a dictionary into a list of (key, sanitized_value) tuples."""
return [(k, safe_filter.cleanse_setting(k, dictionary.get(k))) for k in keys]


def get_stack(context=1) -> list[stubs.InspectStack]:
Expand Down
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Pending
-------

* Added hook to RedirectsPanel for subclass customization.
* Added feature to sanitize sensitive data in the Request Panel.

5.1.0 (2025-03-20)
------------------
Expand Down
73 changes: 73 additions & 0 deletions tests/panels/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,76 @@ def test_session_list_sorted_or_not(self):
self.panel.generate_stats(self.request, response)
panel_stats = self.panel.get_stats()
self.assertEqual(panel_stats["session"], data)

def test_sensitive_post_data_sanitized(self):
"""Test that sensitive POST data is redacted."""
self.request.POST = {"username": "testuser", "password": "secret123"}
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)

# Check that password is redacted in panel content
content = self.panel.content
self.assertIn("username", content)
self.assertIn("testuser", content)
self.assertIn("password", content)
self.assertNotIn("secret123", content)
self.assertIn("********************", content)

def test_sensitive_get_data_sanitized(self):
"""Test that sensitive GET data is redacted."""
self.request.GET = {"api_key": "abc123", "q": "search term"}
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)

# Check that api_key is redacted in panel content
content = self.panel.content
self.assertIn("api_key", content)
self.assertNotIn("abc123", content)
self.assertIn("********************", content)
self.assertIn("q", content)
self.assertIn("search term", content)

def test_sensitive_cookie_data_sanitized(self):
"""Test that sensitive cookie data is redacted."""
self.request.COOKIES = {"session_id": "abc123", "auth_token": "xyz789"}
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)

# Check that auth_token is redacted in panel content
content = self.panel.content
self.assertIn("session_id", content)
self.assertIn("abc123", content)
self.assertIn("auth_token", content)
self.assertNotIn("xyz789", content)
self.assertIn("********************", content)

def test_sensitive_session_data_sanitized(self):
"""Test that sensitive session data is redacted."""
self.request.session = {"user_id": 123, "auth_token": "xyz789"}
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)

# Check that auth_token is redacted in panel content
content = self.panel.content
self.assertIn("user_id", content)
self.assertIn("123", content)
self.assertIn("auth_token", content)
self.assertNotIn("xyz789", content)
self.assertIn("********************", content)

def test_querydict_sanitized(self):
"""Test that sensitive data in QueryDict objects is properly redacted."""
query_dict = QueryDict("username=testuser&password=secret123&token=abc456")
self.request.GET = query_dict
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)

# Check that sensitive data is redacted in panel content
content = self.panel.content
self.assertIn("username", content)
self.assertIn("testuser", content)
self.assertIn("password", content)
self.assertNotIn("secret123", content)
self.assertIn("token", content)
self.assertNotIn("abc456", content)
self.assertIn("********************", content)
62 changes: 62 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest

from django.http import QueryDict
from django.test import override_settings

import debug_toolbar.utils
Expand All @@ -8,6 +9,7 @@
get_stack,
get_stack_trace,
render_stacktrace,
sanitize_and_sort_request_vars,
tidy_stacktrace,
)

Expand Down Expand Up @@ -109,3 +111,63 @@ def __init__(self, value):
rendered_stack_2 = render_stacktrace(stack_2_wrapper.value)
self.assertNotIn("test_locals_value_1", rendered_stack_2)
self.assertIn("test_locals_value_2", rendered_stack_2)


class SanitizeAndSortRequestVarsTestCase(unittest.TestCase):
"""Tests for the sanitize_and_sort_request_vars function."""

def test_dict_sanitization(self):
"""Test sanitization of a regular dictionary."""
test_dict = {
"username": "testuser",
"password": "secret123",
"api_key": "abc123",
}
result = sanitize_and_sort_request_vars(test_dict)

# Convert to dict for easier testing
result_dict = dict(result["list"])

self.assertEqual(result_dict["username"], "testuser")
self.assertEqual(result_dict["password"], "********************")
self.assertEqual(result_dict["api_key"], "********************")

def test_querydict_sanitization(self):
"""Test sanitization of a QueryDict."""
query_dict = QueryDict("username=testuser&password=secret123&api_key=abc123")
result = sanitize_and_sort_request_vars(query_dict)

# Convert to dict for easier testing
result_dict = dict(result["list"])

self.assertEqual(result_dict["username"], "testuser")
self.assertEqual(result_dict["password"], "********************")
self.assertEqual(result_dict["api_key"], "********************")

def test_non_sortable_dict_keys(self):
"""Test dictionary with keys that can't be sorted."""
test_dict = {
1: "one",
"2": "two",
None: "none",
}
result = sanitize_and_sort_request_vars(test_dict)
self.assertEqual(len(result["list"]), 3)
result_dict = dict(result["list"])
self.assertEqual(result_dict[1], "one")
self.assertEqual(result_dict["2"], "two")
self.assertEqual(result_dict[None], "none")

def test_querydict_multiple_values(self):
"""Test QueryDict with multiple values for the same key."""
query_dict = QueryDict("name=bar1&name=bar2&title=value")
result = sanitize_and_sort_request_vars(query_dict)
result_dict = dict(result["list"])
self.assertEqual(result_dict["name"], ["bar1", "bar2"])
self.assertEqual(result_dict["title"], "value")

def test_non_dict_input(self):
"""Test handling of non-dict input."""
test_input = ["not", "a", "dict"]
result = sanitize_and_sort_request_vars(test_input)
self.assertEqual(result["raw"], test_input)