A Django checkout view's crash reports show customers' card numbers; how do you scrub them with sensitive_variables and sensitive_post_parameters?
answer
- two decorators, two places
- POST fields vs frame locals
- call them with parentheses
- only active when DEBUG is off
- exception messages are not scrubbed
basics
~10 sPut @sensitive_post_parameters('card_number', 'cvc') on the view to star those POST fields, and @sensitive_variables('card_number', 'cvc') on functions holding them as locals. Both apply only when DEBUG is False and never touch exception messages.
solid answer
~40 sTwo decorators from `django.views.decorators.debug` mark data for the report filter. `@sensitive_post_parameters("card_number", "cvc")` on the view sets `request.sensitive_post_parameters`, so those fields are starred in the POST section and in any `QueryDict` found in a frame. `@sensitive_variables("card_number", "cvc")` on the function that holds the number stars locals with those names in that frame and in every frame it calls. Both must be called with parentheses, and a class-based view needs `method_decorator`. The filter only acts when `DEBUG` is `False`. It does not scrub exception messages, your own log lines, a caller's locals, or a local with a different name, so never put the number in an exception text, and ideally keep raw card data out of the Django process entirely.
code
python · 22 linesfrom django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.debug import sensitive_post_parameters, sensitive_variables
from .models import Order
from .payments import charge # the project's own payment client
@sensitive_variables("card_number", "cvc")
def pay_for_order(order, card_number, cvc):
# Starred in this frame, in charge()'s frame if it uses the same names,
# and in the wrapper's func_args.
return charge(amount=order.total, card_number=card_number, cvc=cvc)
@method_decorator(sensitive_post_parameters("card_number", "cvc"), name="dispatch")
class CheckoutView(View):
def post(self, request, order_id):
order = get_object_or_404(Order, pk=order_id, customer=request.user)
pay_for_order(order, request.POST["card_number"], request.POST["cvc"])
return redirect("order-receipt", order_id=order.pk)go deeper
Know the two decorators exist in django.views.decorators.debug, that they star values in error reports, and that DEBUG must be off in production.
Explain that one marks POST fields on the request and the other marks local names on a function, and why a class-based view needs method_decorator.
Audit every leak path a report has: frames above the wrapper, renamed locals, exception text, log lines and JSON bodies, and state that the filter is inert while DEBUG is on.
Argue for keeping raw card data out of the application entirely through tokenisation, and treat report scrubbing as a secondary control with an owner and a test.
## Where the card number leaks A Django error report (the ADMINS email, or anything built on `ExceptionReporter`) reproduces three things that can hold a card number: - the **POST section**, which prints `request.POST` field by field; - the **local variables** of each traceback frame (in the HTML report); - the **exception value** and anything your code logged. Django gives you two decorators in `django.views.decorators.debug` to mark the first two. They do no filtering themselves: they leave annotations that the report filter, `SafeExceptionReporterFilter` by default, reads when a report is built, replacing the values with `********************`. ## `sensitive_post_parameters` Applied to a view, it sets `request.sensitive_post_parameters` to the names you listed, or to 'all' when called with no arguments. The filter then: - stars those fields in the report's POST section; - stars them in any `MultiValueDict` (such as `request.POST`) it finds among a frame's locals, which catches `data = request.POST` further down. Rules that trip people up: 1. It must be **called**: `@sensitive_post_parameters()` or with names. Bare `@sensitive_post_parameters` raises `TypeError` when the module is imported. 2. It checks that its first argument is an `HttpRequest`. On a class-based view's method, use `method_decorator(..., name="dispatch")`, or you get a `TypeError` telling you to do so. 3. It covers form-encoded fields only. A JSON body is not in `request.POST`; once parsed into a dict, it is just a local and needs `sensitive_variables`. Django itself applies the no-argument form to the auth views that receive passwords (login, password change, password reset confirm and the auth admin's add and change-password views). ## `sensitive_variables` Applied to any function, it records the variable names you list, or 'all'. When a report is built, the filter walks each traceback frame's callers looking for the decorator's wrapper frame. If it finds one: - locals **with the listed names** are starred in that frame **and in every frame beneath it**, matched by name; - the wrapper frame's own `func_args` and `func_kwargs` are starred, so the arguments passed in do not leak there; - with no names, every local in those frames is starred. Place it at the **top** of a stack of decorators so the arguments are hidden as they pass through the others. Since Django 5.0 both decorators also work on `async def` functions. ## What neither decorator covers | Leak | Why it survives | Fix | |---|---|---| | `raise ValueError(f"declined {card_number}")` | the exception value is printed verbatim | never format card data into messages | | `logger.info("charging %s", card_number)` | your log line is not a report | log a masked form or nothing | | a caller's local holding the number | callers sit **above** the wrapper, not beneath it | decorate the caller too | | a helper's local called `pan` | names are matched literally | use consistent names, or `sensitive_variables()` | | any report while `DEBUG = True` | the filter's `is_active()` is false in debug mode | never run real card data with debug on | The last row is the one to remember in an interview: the decorators protect **production reports**, not the debug page. ## The better fix The decorators are damage control. The strongest answer is architectural: collect card data in the payment provider's hosted field or page so the raw number never reaches your Django process, and your views only ever see a token. Then scrubbing is a second line of defence for tokens and personal data rather than the only thing between a crash and a card number in an inbox. Keep an eye on the environments too: a staging deployment with `DEBUG = True` and copied production data will show every one of these values.
- In Django, why did a card number still appear in an error email even though the view used @sensitive_post_parameters?The decorator only stars POST fields and `QueryDict` locals. The number can still leak as a plain local copied out of the request (`number = request.POST["card_number"]`), as an argument a helper received, inside the exception message, or in a log line. Mark the functions holding it with `sensitive_variables` using the same names, and keep card data out of exception and log text.
- How do you apply Django's sensitive_post_parameters to a class-based view?Wrap it with `method_decorator`, usually on `dispatch`: `@method_decorator(sensitive_post_parameters("card_number"), name="dispatch")`. Applied straight to a method, the decorator receives `self` as its first argument instead of an `HttpRequest` and raises `TypeError`, which tells you to use `method_decorator`.
Like marking fields on a paper form with a black-out stamp request: the clerk who photocopies the form later blacks them out, but only on the fields you marked, and only in the office that follows that rule.
saying these in an interview costs you the question
- sensitive_post_parameters also hides the values on the DEBUG = True page
- Writing @sensitive_variables without parentheses hides every variable
- sensitive_variables on a helper also hides the calling view's locals
- The decorators scrub the card number from exception messages
- sensitive_post_parameters covers fields parsed from a JSON body