How do I filter sensitive Django POST parameters out of Sentry error reports?

django, error-reporting, heroku, sensitive-data, sentry

Solution

Turns out that this stemmed from a bug in Django itself!

If you haven't changed `DEFAULT_EXCEPTION_REPORTER_FILTER` in your `settings` file, you get the default filter of `SafeExceptionReporterFilter`.

If you've used the `sensitive_post_parameters` decorator, this will result in your calling `SafeExceptionReporterFilter`'s `get_post_parameters` method:

 def get_post_parameters(self, request):
        """
        Replaces the values of POST parameters marked as sensitive with
        stars (*********).
        """
        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, v in cleansed.items():
                        cleansed[k] = CLEANSED_SUBSTITUTE
                    return cleansed
                else:
                    # Cleanse only the specified parameters.
                    for param in sensitive_post_parameters:
                        if param in cleansed:
                            cleansed[param] = CLEANSED_SUBSTITUTE
                    return cleansed
            else:
                return request.POST

The problem with the above is that while it will correctly return a `QuerySet` with the sensitive `POST` parameters set to `CLEANSED_SUBSTITUTE` (`'********************'`)...it won't in any way alter `request.body`.

This is a problem when working with Raven/Sentry for Django, because it turns out that the `get_data_from_request` method of Raven's `DjangoClient` first attempts to get the request's `POST` parameters from `request.body`:

def get_data_from_request(self, request):

  [snip]

    if request.method != 'GET':
        try:
            data = request.body
        except Exception:
            try:
                data = request.raw_post_data
            except Exception:
                # assume we had a partial read.
                try:
                    data = request.POST or '<unavailable>'
                except Exception:
                    data = '<unavailable>'
    else:
        data = None

 [snip]

The fastest fix turned out to just involve subclassing `DjangoClient` and manually replacing its output with the cleansed `QuerySet` produced by `SafeExceptionReporterFilter`:

from django.views.debug import SafeExceptionReporterFilter
from raven.contrib.django.client import DjangoClient


class SafeDjangoClient(DjangoClient):

  def get_data_from_request(self, request):
    request.POST = SafeExceptionReporterFilter().get_post_parameters(request)
    result = super(SafeDjangoClient, self).get_data_from_request(request)
    result['sentry.interfaces.Http']['data'] = request.POST
    return result

Problem

To quote the Django docs: ``` @sensitive_post_parameters('pass_word', 'credit_card_number') def record_user_profile(request): UserProfile.create(user=request.user, password=request.POST['pass_word'], credit_card=request.POST['credit_card_number'], name=request.POST['name']) ``` In the above example, the values for the pass_word and credit_card_number POST parameters will be hidden and replaced with stars (******) in the request’s representation inside the error reports, whereas the value of the name parameter will be disclosed. To systematically hide all POST parameters of a request in error reports, do not provide any argument to the sensitive_post_parameters decorator: ``` @sensitive_post_parameters() def my_view(request): ... ``` As a test, I added the following code to my Django 1.6 application: views.py: ``` @sensitive_post_parameters('sensitive') def sensitive(request): if request.method == 'POST': raise IntegrityError(unicode(timezone.now())) return render(request, 'sensitive-test.html', {'form': forms.SensitiveParamForm()}) ``` forms.py: ``` class SensitiveParamForm(forms.Form): not_sensitive = forms.CharField(max_length=255) sensitive = forms.CharField(max_length=255) ``` When I submit this form via `POST`, I can see the values of both fields (including `sensitive`) clear as day in the Sentry report. What am I doing wrong here? I'm using Django 1.6 and Raven 3.5.2. Thanks in advance for your help!

Original source