Permalink
Browse files

Fixed #14614 - filtering of sensitive information in 500 error reports.

This adds a flexible mechanism for filtering what request/traceback
information is shown in 500 error emails and logs. It also applies
screening to some views known to be sensitive e.g. views that handle
passwords.

Thanks to oaylanc for the report and many thanks to Julien Phalip for the
patch and the rest of the work on this.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16339 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent bb12a02 commit 45e55b91435541203f517770c654675f67fa6a3b @spookylukey spookylukey committed Jun 8, 2011
@@ -537,6 +537,10 @@
}
}
+# Default exception reporter filter class used in case none has been
+# specifically assigned to the HttpRequest instance.
+DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFilter'
+
###########
# TESTING #
###########
@@ -12,6 +12,7 @@
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext, ugettext_lazy as _
from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.debug import sensitive_post_parameters
csrf_protect_m = method_decorator(csrf_protect)
@@ -78,6 +79,7 @@ def get_urls(self):
(r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password))
) + super(UserAdmin, self).get_urls()
+ @sensitive_post_parameters()
@csrf_protect_m
@transaction.commit_on_success
def add_view(self, request, form_url='', extra_context=None):
@@ -102,6 +104,7 @@ def add_view(self, request, form_url='', extra_context=None):
extra_context.update(defaults)
return super(UserAdmin, self).add_view(request, form_url, extra_context)
+ @sensitive_post_parameters()
def user_change_password(self, request, id):
if not self.has_change_permission(request):
raise PermissionDenied
@@ -6,6 +6,7 @@
from django.template.response import TemplateResponse
from django.utils.http import base36_to_int
from django.utils.translation import ugettext as _
+from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
@@ -18,6 +19,7 @@
from django.contrib.sites.models import get_current_site
+@sensitive_post_parameters()
@csrf_protect
@never_cache
def login(request, template_name='registration/login.html',
@@ -175,6 +177,7 @@ def password_reset_done(request,
current_app=current_app)
# Doesn't need csrf_protect since no-one can guess the URL
+@sensitive_post_parameters()
@never_cache
def password_reset_confirm(request, uidb36=None, token=None,
template_name='registration/password_reset_confirm.html',
@@ -227,6 +230,7 @@ def password_reset_complete(request,
return TemplateResponse(request, template_name, context,
current_app=current_app)
+@sensitive_post_parameters()
@csrf_protect
@login_required
def password_change(request,
@@ -206,7 +206,7 @@ def handle_uncaught_exception(self, request, resolver, exc_info):
exc_info=exc_info,
extra={
'status_code': 500,
- 'request':request
+ 'request': request
}
)
View
@@ -1,6 +1,10 @@
import logging
import sys
+import traceback
+
+from django.conf import settings
from django.core import mail
+from django.views.debug import ExceptionReporter, get_exception_reporter_filter
# Make sure a NullHandler is available
# This was added in Python 2.7/3.2
@@ -35,29 +39,25 @@ def __init__(self, include_html=False):
"""An exception log handler that emails log entries to site admins.
If the request is passed as the first argument to the log record,
- request data will be provided in the
+ request data will be provided in the email report.
"""
def emit(self, record):
- import traceback
- from django.conf import settings
- from django.views.debug import ExceptionReporter
-
try:
request = record.request
subject = '%s (%s IP): %s' % (
record.levelname,
(request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'),
record.msg
)
- request_repr = repr(request)
+ filter = get_exception_reporter_filter(request)
@zborboa-g
zborboa-g Nov 24, 2014

"Redefining built-in 'filter' [redefined-builtin]"

+ request_repr = filter.get_request_repr(request)
except:
subject = '%s: %s' % (
record.levelname,
record.msg
)
-
request = None
- request_repr = "Request repr() unavailable"
+ request_repr = "Request repr() unavailable."
if record.exc_info:
exc_info = record.exc_info
View
@@ -3,9 +3,11 @@
import re
import sys
import types
+from pprint import pformat
from django.conf import settings
-from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
+from django.http import (HttpResponse, HttpResponseServerError,
+ HttpResponseNotFound, HttpRequest)
from django.template import (Template, Context, TemplateDoesNotExist,
TemplateSyntaxError)
from django.template.defaultfilters import force_escape, pprint
@@ -15,6 +17,8 @@
HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD|PROFANITIES_LIST|SIGNATURE')
+CLEANSED_SUBSTITUTE = u'********************'
+
def linebreak_iter(template_source):
yield 0
p = template_source.find('\n')
@@ -31,7 +35,7 @@ def cleanse_setting(key, value):
"""
try:
if HIDDEN_SETTINGS.search(key):
- cleansed = '********************'
+ cleansed = CLEANSED_SUBSTITUTE
else:
if isinstance(value, dict):
cleansed = dict((k, cleanse_setting(k, v)) for k,v in value.items())
@@ -59,12 +63,158 @@ def technical_500_response(request, exc_type, exc_value, tb):
html = reporter.get_traceback_html()
return HttpResponseServerError(html, mimetype='text/html')
+# Cache for the default exception reporter filter instance.
+default_exception_reporter_filter = None
+
+def get_exception_reporter_filter(request):
+ global default_exception_reporter_filter
+ if default_exception_reporter_filter is None:
+ # Load the default filter for the first time and cache it.
+ modpath = settings.DEFAULT_EXCEPTION_REPORTER_FILTER
+ modname, classname = modpath.rsplit('.', 1)
+ try:
+ mod = import_module(modname)
+ except ImportError, e:
+ raise ImproperlyConfigured(
+ 'Error importing default exception reporter filter %s: "%s"' % (modpath, e))
+ try:
+ default_exception_reporter_filter = getattr(mod, classname)()
+ except AttributeError:
+ raise exceptions.ImproperlyConfigured('Default exception reporter filter module "%s" does not define a "%s" class' % (modname, classname))
+ if request:
+ return getattr(request, 'exception_reporter_filter', default_exception_reporter_filter)
+ else:
+ return default_exception_reporter_filter
+
+class ExceptionReporterFilter(object):
+ """
+ Base for all exception reporter filter classes. All overridable hooks
+ contain lenient default behaviours.
+ """
+
+ def get_request_repr(self, request):
+ if request is None:
+ return repr(None)
+ else:
+ # Since this is called as part of error handling, we need to be very
+ # robust against potentially malformed input.
+ try:
+ get = pformat(request.GET)
+ except:
+ get = '<could not parse>'
+ if request._post_parse_error:
+ post = '<could not parse>'
+ else:
+ try:
+ post = pformat(self.get_post_parameters(request))
+ except:
+ post = '<could not parse>'
+ try:
+ cookies = pformat(request.COOKIES)
+ except:
+ cookies = '<could not parse>'
+ try:
+ meta = pformat(request.META)
+ except:
+ meta = '<could not parse>'
+ return smart_str(u'<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' %
+ (request.__class__.__name__,
+ request.path,
+ unicode(get),
+ unicode(post),
+ unicode(cookies),
+ unicode(meta)))
+
+ def get_post_parameters(self, request):
+ if request is None:
+ return {}
+ else:
+ return request.POST
+
+ def get_traceback_frame_variables(self, request, tb_frame):
+ return tb_frame.f_locals.items()
+
+class SafeExceptionReporterFilter(ExceptionReporterFilter):
+ """
+ Use annotations made by the sensitive_post_parameters and
+ sensitive_variables decorators to filter out sensitive information.
+ """
+
+ def is_active(self, request):
+ """
+ This filter is to add safety in production environments (i.e. DEBUG
+ is False). If DEBUG is True then your site is not safe anyway.
+ This hook is provided as a convenience to easily activate or
+ deactivate the filter on a per request basis.
+ """
+ return settings.DEBUG is False
+
+ 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 cleansed.has_key(param):
+ cleansed[param] = CLEANSED_SUBSTITUTE
+ return cleansed
+ else:
+ return request.POST
+
+ def get_traceback_frame_variables(self, request, tb_frame):
+ """
+ Replaces the values of variables marked as sensitive with
+ stars (*********).
+ """
+ func_name = tb_frame.f_code.co_name
+ func = tb_frame.f_globals.get(func_name)
+ sensitive_variables = getattr(func, 'sensitive_variables', [])
+ cleansed = []
+ if self.is_active(request) and sensitive_variables:
+ if sensitive_variables == '__ALL__':
+ # Cleanse all variables
+ for name, value in tb_frame.f_locals.items():
+ cleansed.append((name, CLEANSED_SUBSTITUTE))
+ return cleansed
+ else:
+ # Cleanse specified variables
+ for name, value in tb_frame.f_locals.items():
+ if name in sensitive_variables:
+ value = CLEANSED_SUBSTITUTE
+ elif isinstance(value, HttpRequest):
+ # Cleanse the request's POST parameters.
+ value = self.get_request_repr(value)
+ cleansed.append((name, value))
+ return cleansed
+ else:
+ # Potentially cleanse only the request if it's one of the frame variables.
+ for name, value in tb_frame.f_locals.items():
+ if isinstance(value, HttpRequest):
+ # Cleanse the request's POST parameters.
+ value = self.get_request_repr(value)
+ cleansed.append((name, value))
+ return cleansed
+
class ExceptionReporter(object):
"""
A class to organize and coordinate reporting on exceptions.
"""
def __init__(self, request, exc_type, exc_value, tb, is_email=False):
self.request = request
+ self.filter = get_exception_reporter_filter(self.request)
self.exc_type = exc_type
self.exc_value = exc_value
self.tb = tb
@@ -124,6 +274,7 @@ def get_traceback_html(self):
'unicode_hint': unicode_hint,
'frames': frames,
'request': self.request,
+ 'filtered_POST': self.filter.get_post_parameters(self.request),
'settings': get_safe_settings(),
'sys_executable': sys.executable,
'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
@@ -222,7 +373,7 @@ def get_traceback_frames(self):
frames = []
tb = self.tb
while tb is not None:
- # support for __traceback_hide__ which is used by a few libraries
+ # Support for __traceback_hide__ which is used by a few libraries
# to hide internal frames.
if tb.tb_frame.f_locals.get('__traceback_hide__'):
tb = tb.tb_next
@@ -239,7 +390,7 @@ def get_traceback_frames(self):
'filename': filename,
'function': function,
'lineno': lineno + 1,
- 'vars': tb.tb_frame.f_locals.items(),
+ 'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame),
'id': id(tb),
'pre_context': pre_context,
'context_line': context_line,
@@ -643,7 +794,7 @@ def empty_urlconf(request):
{% endif %}
<h3 id="post-info">POST</h3>
- {% if request.POST %}
+ {% if filtered_POST %}
<table class="req">
<thead>
<tr>
@@ -652,7 +803,7 @@ def empty_urlconf(request):
</tr>
</thead>
<tbody>
- {% for var in request.POST.items %}
+ {% for var in filtered_POST.items %}
<tr>
<td>{{ var.0 }}</td>
<td class="code"><pre>{{ var.1|pprint }}</pre></td>
Oops, something went wrong. Retry.

0 comments on commit 45e55b9

Please sign in to comment.