Ticket #14614: 14614.exception-reporter-filter.3.diff

File 14614.exception-reporter-filter.3.diff, 44.0 KB (added by Julien Phalip, 13 years ago)
  • django/conf/global_settings.py

    diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
    index 288dc9a..1300e86 100644
    a b LOGGING = {  
    537537    }
    538538}
    539539
     540# Default exception reporter filter class used in case none has been
     541# specifically assigned to the HttpRequest instance.
     542DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFilter'
     543
    540544###########
    541545# TESTING #
    542546###########
  • django/contrib/auth/admin.py

    diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
    index 7d855d8..237b153 100644
    a b from django.utils.html import escape  
    1212from django.utils.decorators import method_decorator
    1313from django.utils.translation import ugettext, ugettext_lazy as _
    1414from django.views.decorators.csrf import csrf_protect
     15from django.views.decorators.debug import sensitive_post_parameters
    1516
    1617csrf_protect_m = method_decorator(csrf_protect)
    1718
    class UserAdmin(admin.ModelAdmin):  
    7879            (r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password))
    7980        ) + super(UserAdmin, self).get_urls()
    8081
     82    @sensitive_post_parameters()
    8183    @csrf_protect_m
    8284    @transaction.commit_on_success
    8385    def add_view(self, request, form_url='', extra_context=None):
    class UserAdmin(admin.ModelAdmin):  
    102104        extra_context.update(defaults)
    103105        return super(UserAdmin, self).add_view(request, form_url, extra_context)
    104106
     107    @sensitive_post_parameters()
    105108    def user_change_password(self, request, id):
    106109        if not self.has_change_permission(request):
    107110            raise PermissionDenied
  • django/contrib/auth/views.py

    diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
    index cfb2659..6a97ffa 100644
    a b from django.http import HttpResponseRedirect, QueryDict  
    66from django.template.response import TemplateResponse
    77from django.utils.http import base36_to_int
    88from django.utils.translation import ugettext as _
     9from django.views.decorators.debug import sensitive_post_parameters
    910from django.views.decorators.cache import never_cache
    1011from django.views.decorators.csrf import csrf_protect
    1112
    from django.contrib.auth.models import User  
    1718from django.contrib.auth.tokens import default_token_generator
    1819from django.contrib.sites.models import get_current_site
    1920
    20 
     21@sensitive_post_parameters()
    2122@csrf_protect
    2223@never_cache
    2324def login(request, template_name='registration/login.html',
    def password_reset_done(request,  
    175176                            current_app=current_app)
    176177
    177178# Doesn't need csrf_protect since no-one can guess the URL
     179@sensitive_post_parameters()
    178180@never_cache
    179181def password_reset_confirm(request, uidb36=None, token=None,
    180182                           template_name='registration/password_reset_confirm.html',
    def password_reset_complete(request,  
    227229    return TemplateResponse(request, template_name, context,
    228230                            current_app=current_app)
    229231
     232@sensitive_post_parameters()
    230233@csrf_protect
    231234@login_required
    232235def password_change(request,
  • django/core/handlers/base.py

    diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
    index d653860..7cae42e 100644
    a b class BaseHandler(object):  
    206206            exc_info=exc_info,
    207207            extra={
    208208                'status_code': 500,
    209                 'request':request
     209                'request': request
    210210            }
    211211        )
    212212
  • django/utils/log.py

    diff --git a/django/utils/log.py b/django/utils/log.py
    index 93e38d1..c1cd328 100644
    a b  
    11import logging
    22import sys
     3import traceback
     4
     5from django.conf import settings
    36from django.core import mail
     7from django.views.debug import ExceptionReporter, get_exception_reporter_filter
    48
    59# Make sure a NullHandler is available
    610# This was added in Python 2.7/3.2
    class AdminEmailHandler(logging.Handler):  
    3539    """An exception log handler that emails log entries to site admins.
    3640
    3741    If the request is passed as the first argument to the log record,
    38     request data will be provided in the
     42    request data will be provided in the email report.
    3943    """
    4044    def emit(self, record):
    41         import traceback
    42         from django.conf import settings
    43         from django.views.debug import ExceptionReporter
    44 
    4545        try:
    4646            request = record.request
    4747            subject = '%s (%s IP): %s' % (
    class AdminEmailHandler(logging.Handler):  
    4949                (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'),
    5050                record.msg
    5151            )
    52             request_repr = repr(request)
     52            filter = get_exception_reporter_filter(request)
     53            request_repr = filter.get_request_repr(request)
    5354        except:
    5455            subject = '%s: %s' % (
    5556                record.levelname,
    5657                record.msg
    5758            )
    58 
    5959            request = None
    60             request_repr = "Request repr() unavailable"
    61 
     60            request_repr = "Request repr() unavailable."
     61               
    6262        if record.exc_info:
    6363            exc_info = record.exc_info
    6464            stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
  • django/views/debug.py

    diff --git a/django/views/debug.py b/django/views/debug.py
    index 67f25b3..5e3900c 100644
    a b import os  
    33import re
    44import sys
    55import types
     6from pprint import pformat
    67
    78from django.conf import settings
    8 from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
     9from django.http import (HttpResponse, HttpResponseServerError,
     10    HttpResponseNotFound, HttpRequest)
    911from django.template import (Template, Context, TemplateDoesNotExist,
    1012    TemplateSyntaxError)
    1113from django.template.defaultfilters import force_escape, pprint
    from django.utils.encoding import smart_unicode, smart_str  
    1517
    1618HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD|PROFANITIES_LIST|SIGNATURE')
    1719
     20CLEANSED_SUBSTITUTE = u'********************'
     21
    1822def linebreak_iter(template_source):
    1923    yield 0
    2024    p = template_source.find('\n')
    def cleanse_setting(key, value):  
    3135    """
    3236    try:
    3337        if HIDDEN_SETTINGS.search(key):
    34             cleansed = '********************'
     38            cleansed = CLEANSED_SUBSTITUTE
    3539        else:
    3640            if isinstance(value, dict):
    3741                cleansed = dict((k, cleanse_setting(k, v)) for k,v in value.items())
    def technical_500_response(request, exc_type, exc_value, tb):  
    5963    html = reporter.get_traceback_html()
    6064    return HttpResponseServerError(html, mimetype='text/html')
    6165
     66# Cache for the default exception reporter filter instance.
     67default_exception_reporter_filter = None
     68
     69def get_exception_reporter_filter(request):
     70    global default_exception_reporter_filter
     71    if default_exception_reporter_filter is None:
     72        # Load the default filter for the first time and cache it.
     73        modpath = settings.DEFAULT_EXCEPTION_REPORTER_FILTER
     74        modname, classname = modpath.rsplit('.', 1)
     75        try:
     76            mod = import_module(modname)
     77        except ImportError, e:
     78            raise ImproperlyConfigured(
     79            'Error importing default exception reporter filter %s: "%s"' % (modpath, e))
     80        try:
     81            default_exception_reporter_filter = getattr(mod, classname)()
     82        except AttributeError:
     83            raise exceptions.ImproperlyConfigured('Default exception reporter filter module "%s" does not define a "%s" class' % (modname, classname))
     84    if request:
     85        return getattr(request, 'exception_reporter_filter', default_exception_reporter_filter)
     86    else:
     87        return default_exception_reporter_filter
     88
     89class ExceptionReporterFilter(object):
     90    """
     91    Base for all exception reporter filter classes. All overridable hooks
     92    contain lenient default behaviours.
     93    """
     94   
     95    def get_request_repr(self, request):
     96        if request is None:
     97            return repr(None)
     98        else:
     99            # Since this is called as part of error handling, we need to be very
     100            # robust against potentially malformed input.
     101            try:
     102                get = pformat(request.GET)
     103            except:
     104                get = '<could not parse>'
     105            if request._post_parse_error:
     106                post = '<could not parse>'
     107            else:
     108                try:
     109                    post = pformat(self.get_post_parameters(request))
     110                except:
     111                    post = '<could not parse>'
     112            try:
     113                cookies = pformat(request.COOKIES)
     114            except:
     115                cookies = '<could not parse>'
     116            try:
     117                meta = pformat(request.META)
     118            except:
     119                meta = '<could not parse>'
     120            return smart_str(u'<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' %
     121                             (request.__class__.__name__,
     122                              request.path,
     123                              unicode(get),
     124                              unicode(post),
     125                              unicode(cookies),
     126                              unicode(meta)))
     127
     128    def get_post_parameters(self, request):
     129        if request is None:
     130            return {}
     131        else:
     132            return request.POST
     133           
     134    def get_traceback_frame_variables(self, request, tb_frame):
     135        return tb_frame.f_locals.items()
     136
     137class SafeExceptionReporterFilter(ExceptionReporterFilter):
     138    """
     139    Use annotations made by the sensitive_post_parameters and
     140    sensitive_variables decorators to filter out sensitive information.
     141    """
     142   
     143    def is_active(self, request):
     144        """
     145        This filter is to add safety in production environments (i.e. DEBUG
     146        is False). If DEBUG is True then your site is not safe anyway.
     147        This hook is provided as a convenience to easily activate or
     148        deactivate the filter on a per request basis.
     149        """
     150        return settings.DEBUG is False
     151   
     152    def get_post_parameters(self, request):
     153        """
     154        Replaces the values of POST parameters marked as sensitive with
     155        stars (*********).
     156        """
     157        if request is None:
     158            return {}
     159        else:
     160            sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
     161            if self.is_active(request) and sensitive_post_parameters:
     162                cleansed = request.POST.copy()
     163                if sensitive_post_parameters == '__ALL__':
     164                    # Cleanse all parameters.
     165                    for k, v in cleansed.items():
     166                        cleansed[k] = CLEANSED_SUBSTITUTE
     167                    return cleansed
     168                else:
     169                    # Cleanse only the specified parameters.
     170                    for param in sensitive_post_parameters:
     171                        if cleansed.has_key(param):
     172                            cleansed[param] = CLEANSED_SUBSTITUTE
     173                    return cleansed
     174            else:
     175                return request.POST
     176   
     177    def get_traceback_frame_variables(self, request, tb_frame):
     178        """
     179        Replaces the values of variables marked as sensitive with
     180        stars (*********).
     181        """
     182        func_name = tb_frame.f_code.co_name
     183        func = tb_frame.f_globals.get(func_name)
     184        sensitive_variables = getattr(func, 'sensitive_variables', [])
     185        cleansed = []
     186        if self.is_active(request) and sensitive_variables:
     187            if sensitive_variables == '__ALL__':
     188                # Cleanse all variables
     189                for name, value in tb_frame.f_locals.items():
     190                    cleansed.append((name, CLEANSED_SUBSTITUTE))
     191                return cleansed
     192            else:
     193                # Cleanse specified variables
     194                for name, value in tb_frame.f_locals.items():
     195                    if name in sensitive_variables:
     196                        value = CLEANSED_SUBSTITUTE
     197                    elif isinstance(value, HttpRequest):
     198                        # Cleanse the request's POST parameters.
     199                        value = self.get_request_repr(value)
     200                    cleansed.append((name, value))
     201                return cleansed
     202        else:
     203            # Potentially cleanse only the request if it's one of the frame variables.
     204            for name, value in tb_frame.f_locals.items():
     205                if isinstance(value, HttpRequest):
     206                    # Cleanse the request's POST parameters.
     207                    value = self.get_request_repr(value)
     208                cleansed.append((name, value))
     209            return cleansed
     210   
    62211class ExceptionReporter(object):
    63212    """
    64213    A class to organize and coordinate reporting on exceptions.
    65214    """
    66215    def __init__(self, request, exc_type, exc_value, tb, is_email=False):
    67216        self.request = request
     217        self.filter = get_exception_reporter_filter(self.request)
    68218        self.exc_type = exc_type
    69219        self.exc_value = exc_value
    70220        self.tb = tb
    class ExceptionReporter(object):  
    124274            'unicode_hint': unicode_hint,
    125275            'frames': frames,
    126276            'request': self.request,
     277            'filtered_POST': self.filter.get_post_parameters(self.request),
    127278            'settings': get_safe_settings(),
    128279            'sys_executable': sys.executable,
    129280            'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
    class ExceptionReporter(object):  
    222373        frames = []
    223374        tb = self.tb
    224375        while tb is not None:
    225             # support for __traceback_hide__ which is used by a few libraries
     376            # Support for __traceback_hide__ which is used by a few libraries
    226377            # to hide internal frames.
    227378            if tb.tb_frame.f_locals.get('__traceback_hide__'):
    228379                tb = tb.tb_next
    class ExceptionReporter(object):  
    239390                    'filename': filename,
    240391                    'function': function,
    241392                    'lineno': lineno + 1,
    242                     'vars': tb.tb_frame.f_locals.items(),
     393                    'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame),
    243394                    'id': id(tb),
    244395                    'pre_context': pre_context,
    245396                    'context_line': context_line,
    class ExceptionReporter(object):  
    247398                    'pre_context_lineno': pre_context_lineno + 1,
    248399                })
    249400            tb = tb.tb_next
    250 
    251401        return frames
    252402
    253403    def format_exception(self):
    Exception Value: {{ exception_value|force_escape }}  
    643793  {% endif %}
    644794
    645795  <h3 id="post-info">POST</h3>
    646   {% if request.POST %}
     796  {% if filtered_POST %}
    647797    <table class="req">
    648798      <thead>
    649799        <tr>
    Exception Value: {{ exception_value|force_escape }}  
    652802        </tr>
    653803      </thead>
    654804      <tbody>
    655         {% for var in request.POST.items %}
     805        {% for var in filtered_POST.items %}
    656806          <tr>
    657807            <td>{{ var.0 }}</td>
    658808            <td class="code"><pre>{{ var.1|pprint }}</pre></td>
  • new file django/views/decorators/debug.py

    diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py
    new file mode 100644
    index 0000000..9974629
    - +  
     1import functools
     2
     3
     4def sensitive_variables(*variables):
     5    """
     6    Indicates which variables used in the decorated function are sensitive, so
     7    that those variables can later be treated in a special way, for example
     8    by hiding them when logging unhandled exceptions.
     9   
     10    Two forms are accepted:
     11   
     12    * with specified variable names:
     13   
     14        @sensitive_variables('user', 'password', 'credit_card')
     15        def my_function(user):
     16            password = user.pass_word
     17            credit_card = user.credit_card_number
     18            ...
     19           
     20    * without any specified variable names, in which case it is assumed that
     21      all variables are considered sensitive:
     22     
     23        @sensitive_variables()
     24        def my_function()
     25            ...
     26    """
     27    def decorator(func):
     28        @functools.wraps(func)
     29        def wrapper(*args, **kwargs):
     30            if variables:
     31                wrapper.sensitive_variables = variables
     32            else:
     33                wrapper.sensitive_variables = '__ALL__'
     34            return func(*args, **kwargs)
     35        return wrapper
     36    return decorator
     37
     38
     39def sensitive_post_parameters(*parameters):
     40    """
     41    Indicates which POST parameters used in the decorated view are sensitive,
     42    so that those parameters can later be treated in a special way, for example
     43    by hiding them when logging unhandled exceptions.
     44   
     45    Two forms are accepted:
     46   
     47    * with specified parameters:
     48   
     49        @sensitive_post_parameters('password', 'credit_card')
     50        def my_view(request):
     51            pw = request.POST['password']
     52            cc = request.POST['credit_card']
     53            ...
     54           
     55    * without any specified parameters, in which case it is assumed that
     56      all parameters are considered sensitive:
     57     
     58        @sensitive_post_parameters()
     59        def my_view(request)
     60            ...
     61    """
     62    def decorator(view):
     63        @functools.wraps(view)
     64        def wrapper(request, *args, **kwargs):
     65            if parameters:
     66                request.sensitive_post_parameters = parameters
     67            else:
     68                request.sensitive_post_parameters = '__ALL__'
     69            return view(request, *args, **kwargs)
     70        return wrapper
     71    return decorator
  • docs/howto/error-reporting.txt

    diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt
    index c15c1d8..f87c57c 100644
    a b  
    1 Error reporting via email
    2 =========================
     1Error reporting
     2===============
    33
    44When you're running a public site you should always turn off the
    55:setting:`DEBUG` setting. That will make your server run much faster, and will
    revealed by the error pages.  
    99However, running with :setting:`DEBUG` set to ``False`` means you'll never see
    1010errors generated by your site -- everyone will just see your public error pages.
    1111You need to keep track of errors that occur in deployed sites, so Django can be
    12 configured to email you details of those errors.
     12configured to create reports with details about those errors.
    1313
    14 Server errors
     14Email reports
    1515-------------
    1616
     17Server errors
     18~~~~~~~~~~~~~
     19
    1720When :setting:`DEBUG` is ``False``, Django will email the users listed in the
    1821:setting:`ADMINS` setting whenever your code raises an unhandled exception and
    1922results in an internal server error (HTTP status code 500). This gives the
    setting.  
    4851   </topics/logging>`.
    4952
    5053404 errors
    51 ----------
     54~~~~~~~~~~
    5255
    5356Django can also be configured to email errors about broken links (404 "page
    5457not found" errors). Django sends emails about 404 errors when:
    The best way to disable this behavior is to set  
    9699
    97100.. seealso::
    98101
    99     You can also set up custom error reporting by writing a custom piece of
    100     :ref:`exception middleware <exception-middleware>`. If you do write custom
    101     error handling, it's a good idea to emulate Django's built-in error handling
    102     and only report/log errors if :setting:`DEBUG` is ``False``.
    103 
    104 .. seealso::
    105 
    106102   .. versionadded:: 1.3
    107103
    108104   404 errors are logged using the logging framework. By default, these log
    The best way to disable this behavior is to set  
    116112   Previously, two settings were used to control which URLs not to report:
    117113   :setting:`IGNORABLE_404_STARTS` and :setting:`IGNORABLE_404_ENDS`. They
    118114   were replaced by :setting:`IGNORABLE_404_URLS`.
     115
     116.. _filtering-error-reports:
     117
     118Filtering error reports
     119-----------------------
     120
     121.. versionadded:: 1.4
     122
     123Filtering sensitive information
     124~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     125
     126Error reports are really helpful for debugging errors, so it is generally
     127useful to record as much relevant information about those errors as possible.
     128For example, by default Django records the `full traceback`_ for the
     129exception raised, each `traceback frame`_'s local variables, and the
     130:class:`HttpRequest`'s :ref:`attributes<httprequest-attributes>`.
     131
     132However, sometimes certain types of information may be too sensitive and thus
     133may not be appropriate to be kept track of, for example a user's password or
     134credit card number. So Django offers a set of function decorators to help you
     135control which information should be filtered out of error reports in a
     136production environment (that is, where :setting:`DEBUG` is set to ``False``):
     137:func:`sensitive_variables` and :func:`sensitive_post_parameters`.
     138
     139.. _`full traceback`: http://en.wikipedia.org/wiki/Stack_trace
     140.. _`traceback frame`: http://en.wikipedia.org/wiki/Stack_frame
     141
     142.. function:: sensitive_variables(*variables)
     143
     144    If a function (either a view or any regular callback) in your code uses
     145    local variables susceptible to contain sensitive information, you may
     146    prevent the values of those variables from being included in error reports
     147    using the ``sensitive_variables`` decorator:
     148   
     149        .. code-block:: python
     150       
     151            from django.views.decorators.debug import sensitive_variables
     152       
     153            @sensitive_variables('user', 'pw', 'cc')
     154            def process_info(user):
     155                pw = user.pass_word
     156                cc = user.credit_card_number
     157                name = user.name
     158                ...
     159                   
     160    In the above example, the values for the ``user``, ``pw`` and ``cc``
     161    variables will be hidden and replaced with stars (`**********`) in the
     162    error reports, whereas the value of the ``name`` variable will be
     163    disclosed.
     164
     165    To systematically hide all local variables of a function from error logs,
     166    do not provide any argument to the ``sensitive_variables`` decorator:
     167   
     168        .. code-block:: python
     169
     170            @sensitive_variables()
     171            def my_function():
     172                ...
     173
     174.. function:: sensitive_post_parameters(*parameters)
     175
     176    If one of your views receives an :class:`HttpRequest` object with
     177    :attr:`POST parameters<HttpRequest.POST>` susceptible to contain sensitive
     178    information, you may prevent the values of those parameters from being
     179    included in the error reports using the ``sensitive_post_parameters``
     180    decorator:
     181   
     182        .. code-block:: python
     183       
     184            from django.views.decorators.debug import sensitive_post_parameters
     185       
     186            @sensitive_post_parameters('pass_word', 'credit_card_number')
     187            def record_user_profile(request):
     188                UserProfile.create(user=request.user,
     189                                   password=request.POST['pass_word'],
     190                                   credit_card=request.POST['credit_card_number'],
     191                                   name=request.POST['name'])
     192                ...
     193               
     194    In the above example, the values for the ``pass_word`` and
     195    ``credit_card_number`` POST parameters will be hidden and replaced with
     196    stars (`**********`) in the request's representation inside the error
     197    reports, whereas the value of the ``name`` parameter will be disclosed.
     198
     199    To systematically hide all POST parameters of a request in error reports,
     200    do not provide any argument to the ``sensitive_post_parameters`` decorator:
     201   
     202        .. code-block:: python
     203
     204            @sensitive_post_parameters()
     205            def my_view(request):
     206                ...
     207
     208.. note::
     209
     210    .. versionchanged:: 1.4
     211   
     212    Since version 1.4, all POST parameters are systematically filtered out of
     213    error reports for certain :mod:`contrib.views.auth` views (``login``,
     214    ``password_reset_confirm``, ``password_change``, and ``add_view`` and
     215    ``user_change_password`` in the ``auth`` admin) to prevent the leaking of
     216    sensitive information such as user passwords.
     217
     218.. _custom-error-reports:
     219
     220Custom error reports
     221~~~~~~~~~~~~~~~~~~~~
     222
     223All :func:`sensitive_variables` and :func:`sensitive_post_parameters` do is,
     224respectively, annotate the decorated function with the names of sensitive
     225variables and annotate the ``HttpRequest`` object with the names of sensitive
     226POST parameters, so that this sensitive information can later be filtered out
     227of reports when an error occurs. The actual filtering is done by Django's
     228default error reporter filter:
     229:class:`django.views.debug.SafeExceptionReporterFilter`. This filter uses the
     230decorators' annotations to replace the corresponding values with stars
     231(`**********`) when the error reports are produced. If you wish to override or
     232customize this default behavior for your entire site, you need to define your
     233own filter class and tell Django to use it via the
     234:setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` setting:
     235
     236    .. code-block:: python
     237   
     238        DEFAULT_EXCEPTION_REPORTER_FILTER = 'path.to.your.CustomExceptionReporterFilter'
     239
     240You may also control in a more granular way which filter to use within any
     241given view by setting the ``HttpRequest``'s ``exception_reporter_filter``
     242attribute:
     243
     244    .. code-block:: python
     245
     246        def my_view(request):
     247            if request.user.is_authenticated():
     248                request.exception_reporter_filter = CustomExceptionReporterFilter()
     249            ...
     250
     251Your custom filter class needs to inherit from
     252:class:`django.views.debug.SafeExceptionReporterFilter` and may override the
     253following methods:
     254
     255.. class:: django.views.debug.SafeExceptionReporterFilter
     256   
     257.. method:: SafeExceptionReporterFilter.is_active(self, request)
     258
     259    Returns ``True`` to activate the filtering operated in the other methods.
     260    By default the filter is active if :setting:`DEBUG` is ``False``.
     261   
     262.. method:: SafeExceptionReporterFilter.get_request_repr(self, request)
     263
     264    Returns the representation string of the request object, that is, the
     265    value that would be returned by ``repr(request)``, except it uses the
     266    filtered dictionary of POST parameters as determined by
     267    :meth:`SafeExceptionReporterFilter.get_post_parameters`.
     268
     269.. method:: SafeExceptionReporterFilter.get_post_parameters(self, request)
     270
     271    Returns the filtered dictionary of POST parameters. By default it replaces
     272    the values of sensitive parameters with stars (`**********`).
     273   
     274.. method:: SafeExceptionReporterFilter.get_traceback_frame_variables(self, request, tb_frame)
     275
     276    Returns the filtered dictionary of local variables for the given traceback
     277    frame. By default it replaces the values of sensitive variables with stars
     278    (`**********`).
     279
     280.. seealso::
     281
     282    You can also set up custom error reporting by writing a custom piece of
     283    :ref:`exception middleware <exception-middleware>`. If you do write custom
     284    error handling, it's a good idea to emulate Django's built-in error handling
     285    and only report/log errors if :setting:`DEBUG` is ``False``.
  • docs/ref/request-response.txt

    diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
    index 72872d5..ff04e25 100644
    a b HttpRequest objects  
    2323
    2424.. class:: HttpRequest
    2525
     26.. _httprequest-attributes:
     27
    2628Attributes
    2729----------
    2830
  • docs/ref/settings.txt

    diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
    index 816c3e9..e95c165 100644
    a b Default content type to use for all ``HttpResponse`` objects, if a MIME type  
    772772isn't manually specified. Used with :setting:`DEFAULT_CHARSET` to construct
    773773the ``Content-Type`` header.
    774774
     775.. setting:: DEFAULT_EXCEPTION_REPORTER_FILTER
     776
     777DEFAULT_EXCEPTION_REPORTER_FILTER
     778---------------------------------
     779
     780Default: :class:`django.views.debug.SafeExceptionReporterFilter`
     781
     782Default exception reporter filter class to be used if none has been assigned to
     783the :class:`HttpRequest` instance yet.
     784See :ref:`Filtering error reports<filtering-error-reports>`.
     785
    775786.. setting:: DEFAULT_FILE_STORAGE
    776787
    777788DEFAULT_FILE_STORAGE
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    index bddb15c..a0eb55a 100644
    a b help with AJAX heavy sites, protection for PUT and DELETE, and settings  
    112112the security and usefulness of the CSRF protection. See the :doc:`CSRF docs
    113113</ref/contrib/csrf>` for more information.
    114114
     115Error report filtering
     116~~~~~~~~~~~~~~~~~~~~~~
     117
     118Two new function decorators, :func:`sensitive_variables` and
     119:func:`sensitive_post_parameters`, were added to allow designating the
     120traceback frames' local variables and request's POST parameters susceptible
     121to contain sensitive information and that should be filtered out of error
     122reports.
     123
     124All POST parameters are now systematically filtered out of error reports for
     125certain :mod:`contrib.views.auth` views (``login``, ``password_reset_confirm``,
     126``password_change``, and ``add_view`` and ``user_change_password`` in the
     127``auth`` admin) to prevent the leaking of sensitive information such as user
     128passwords.
     129
     130You may override or customize the default filtering by writing a
     131:ref:`custom filter<custom-error-reports>`. Learn more on
     132:ref:`Filtering error reports<filtering-error-reports>`.
     133
     134
    115135.. _backwards-incompatible-changes-1.4:
    116136
    117137Backwards incompatible changes in 1.4
  • docs/topics/logging.txt

    diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt
    index 651a92f..e2c9f72 100644
    a b Python logging module.  
    504504    sensitive, and you may not want to send it over email. Consider using
    505505    something such as `django-sentry`_ to get the best of both worlds -- the
    506506    rich information of full tracebacks plus the security of *not* sending the
    507     information over email.
     507    information over email. You may also explicitly designate certain
     508    sensitive information to be filtered out of error reports -- learn more on
     509    :ref:`Filtering error reports<filtering-error-reports>`.
    508510
    509511.. _django-sentry: http://pypi.python.org/pypi/django-sentry
  • tests/regressiontests/views/tests/debug.py

    diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py
    index 6dd4bd4..da71aaf 100644
    a b  
     1from __future__ import with_statement
    12import inspect
    23import os
    34import sys
    from django.test import TestCase, RequestFactory  
    89from django.core.urlresolvers import reverse
    910from django.template import TemplateSyntaxError
    1011from django.views.debug import ExceptionReporter
     12from django.core.exceptions import ImproperlyConfigured
     13from django.core import mail
    1114
    1215from regressiontests.views import BrokenException, except_args
     16from regressiontests.views.views import (sensitive_view, non_sensitive_view,
     17    paranoid_view, custom_exception_reporter_filter_view)
    1318
    1419
    1520class DebugViewTests(TestCase):
    class ExceptionReporterTests(TestCase):  
    143148        self.assertNotIn('<h2>Traceback ', html)
    144149        self.assertIn('<h2>Request information</h2>', html)
    145150        self.assertIn('<p>Request data not supplied</p>', html)
     151
     152
     153class ExceptionReporterFilterTests(TestCase):
     154    """
     155    Ensure that sensitive information can be filtered out of error reports.
     156    Refs #14614.
     157    """
     158    rf = RequestFactory()
     159    breakfast_data = {'sausage-key': 'sausage-value',
     160                      'baked-beans-key': 'baked-beans-value',
     161                      'hash-brown-key': 'hash-brown-value',
     162                      'bacon-key': 'bacon-value',}
     163           
     164    def verify_unsafe_response(self, view):
     165        """
     166        Asserts that potentially sensitive info are displayed in the response.
     167        """
     168        request = self.rf.post('/some_url/', self.breakfast_data)
     169        response = view(request)
     170        # All variables are shown.
     171        self.assertContains(response, 'cooked_eggs', status_code=500)
     172        self.assertContains(response, 'scrambled', status_code=500)
     173        self.assertContains(response, 'sauce', status_code=500)
     174        self.assertContains(response, 'worcestershire', status_code=500)
     175        for k, v in self.breakfast_data.items():
     176            # All POST parameters are shown.
     177            self.assertContains(response, k, status_code=500)
     178            self.assertContains(response, v, status_code=500)
     179   
     180    def verify_safe_response(self, view):
     181        """
     182        Asserts that certain sensitive info are not displayed in the response.
     183        """
     184        request = self.rf.post('/some_url/', self.breakfast_data)
     185        response = view(request)
     186        # Non-sensitive variable's name and value are shown.
     187        self.assertContains(response, 'cooked_eggs', status_code=500)
     188        self.assertContains(response, 'scrambled', status_code=500)
     189        # Sensitive variable's name is shown but not its value.
     190        self.assertContains(response, 'sauce', status_code=500)
     191        self.assertNotContains(response, 'worcestershire', status_code=500)
     192        for k, v in self.breakfast_data.items():
     193            # All POST parameters' names are shown.
     194            self.assertContains(response, k, status_code=500)
     195        # Non-sensitive POST parameters' values are shown.
     196        self.assertContains(response, 'baked-beans-value', status_code=500)
     197        self.assertContains(response, 'hash-brown-value', status_code=500)
     198        # Sensitive POST parameters' values are not shown.
     199        self.assertNotContains(response, 'sausage-value', status_code=500)
     200        self.assertNotContains(response, 'bacon-value', status_code=500)
     201   
     202    def verify_paranoid_response(self, view):
     203        """
     204        Asserts that no variables or POST parameters are displayed in the response.
     205        """
     206        request = self.rf.post('/some_url/', self.breakfast_data)
     207        response = view(request)
     208        # Show variable names but not their values.
     209        self.assertContains(response, 'cooked_eggs', status_code=500)
     210        self.assertNotContains(response, 'scrambled', status_code=500)
     211        self.assertContains(response, 'sauce', status_code=500)
     212        self.assertNotContains(response, 'worcestershire', status_code=500)
     213        for k, v in self.breakfast_data.items():
     214            # All POST parameters' names are shown.
     215            self.assertContains(response, k, status_code=500)
     216            # No POST parameters' values are shown.
     217            self.assertNotContains(response, v, status_code=500)
     218       
     219    def verify_unsafe_email(self, view):
     220        """
     221        Asserts that potentially sensitive info are displayed in the email report.
     222        """
     223        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     224            mail.outbox = [] # Empty outbox
     225            request = self.rf.post('/some_url/', self.breakfast_data)
     226            response = view(request)
     227            self.assertEquals(len(mail.outbox), 1)
     228            email = mail.outbox[0]
     229            # Frames vars are never shown in plain text email reports.
     230            self.assertNotIn('cooked_eggs', email.body)
     231            self.assertNotIn('scrambled', email.body)
     232            self.assertNotIn('sauce', email.body)
     233            self.assertNotIn('worcestershire', email.body)
     234            for k, v in self.breakfast_data.items():
     235                # All POST parameters are shown.
     236                self.assertIn(k, email.body)
     237                self.assertIn(v, email.body)
     238       
     239    def verify_safe_email(self, view):
     240        """
     241        Asserts that certain sensitive info are not displayed in the email report.
     242        """
     243        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     244            mail.outbox = [] # Empty outbox
     245            request = self.rf.post('/some_url/', self.breakfast_data)
     246            response = view(request)
     247            self.assertEquals(len(mail.outbox), 1)
     248            email = mail.outbox[0]
     249            # Frames vars are never shown in plain text email reports.
     250            self.assertNotIn('cooked_eggs', email.body)
     251            self.assertNotIn('scrambled', email.body)
     252            self.assertNotIn('sauce', email.body)
     253            self.assertNotIn('worcestershire', email.body)
     254            for k, v in self.breakfast_data.items():
     255                # All POST parameters' names are shown.
     256                self.assertIn(k, email.body)
     257            # Non-sensitive POST parameters' values are shown.
     258            self.assertIn('baked-beans-value', email.body)
     259            self.assertIn('hash-brown-value', email.body)
     260            # Sensitive POST parameters' values are not shown.
     261            self.assertNotIn('sausage-value', email.body)
     262            self.assertNotIn('bacon-value', email.body)
     263   
     264    def verify_paranoid_email(self, view):
     265        """
     266        Asserts that no variables or POST parameters are displayed in the email report.
     267        """
     268        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     269            mail.outbox = [] # Empty outbox
     270            request = self.rf.post('/some_url/', self.breakfast_data)
     271            response = view(request)
     272            self.assertEquals(len(mail.outbox), 1)
     273            email = mail.outbox[0]
     274            # Frames vars are never shown in plain text email reports.
     275            self.assertNotIn('cooked_eggs', email.body)
     276            self.assertNotIn('scrambled', email.body)
     277            self.assertNotIn('sauce', email.body)
     278            self.assertNotIn('worcestershire', email.body)
     279            for k, v in self.breakfast_data.items():
     280                # All POST parameters' names are shown.
     281                self.assertIn(k, email.body)
     282                # No POST parameters' values are shown.
     283                self.assertNotIn(v, email.body)
     284           
     285    def test_non_sensitive_request(self):
     286        """
     287        Ensure that everything (request info and frame variables) can bee seen
     288        in the default error reports for non-sensitive requests.
     289        """
     290        with self.settings(DEBUG=True):
     291            self.verify_unsafe_response(non_sensitive_view)
     292            self.verify_unsafe_email(non_sensitive_view)
     293               
     294        with self.settings(DEBUG=False):
     295            self.verify_unsafe_response(non_sensitive_view)
     296            self.verify_unsafe_email(non_sensitive_view)
     297       
     298    def test_sensitive_request(self):
     299        """
     300        Ensure that sensitive POST parameters and frame variables cannot be
     301        seen in the default error reports for sensitive requests.
     302        """
     303        with self.settings(DEBUG=True):
     304            self.verify_unsafe_response(sensitive_view)
     305            self.verify_unsafe_email(sensitive_view)
     306           
     307        with self.settings(DEBUG=False):
     308            self.verify_safe_response(sensitive_view)
     309            self.verify_safe_email(sensitive_view)
     310   
     311    def test_paranoid_request(self):
     312        """
     313        Ensure that no POST parameters and frame variables can be seen in the
     314        default error reports for "paranoid" requests.
     315        """
     316        with self.settings(DEBUG=True):
     317            self.verify_unsafe_response(paranoid_view)
     318            self.verify_unsafe_email(paranoid_view)
     319           
     320        with self.settings(DEBUG=False):
     321            self.verify_paranoid_response(paranoid_view)
     322            self.verify_paranoid_email(paranoid_view)
     323
     324    def test_custom_exception_reporter_filter(self):
     325        """
     326        Ensure that it's possible to assign an exception reporter filter to
     327        the request to bypass the one set in DEFAULT_EXCEPTION_REPORTER_FILTER.
     328        """
     329        with self.settings(DEBUG=True):
     330            self.verify_unsafe_response(custom_exception_reporter_filter_view)
     331            self.verify_unsafe_email(custom_exception_reporter_filter_view)
     332               
     333        with self.settings(DEBUG=False):
     334            self.verify_unsafe_response(custom_exception_reporter_filter_view)
     335            self.verify_unsafe_email(custom_exception_reporter_filter_view)
  • tests/regressiontests/views/views.py

    diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py
    index 11d289f..6c2f324 100644
    a b from django.http import HttpResponse, HttpResponseRedirect  
    55from django.core.urlresolvers import get_resolver
    66from django.shortcuts import render_to_response, render
    77from django.template import Context, RequestContext, TemplateDoesNotExist
    8 from django.views.debug import technical_500_response
     8from django.views.debug import technical_500_response, SafeExceptionReporterFilter
     9from django.views.decorators.debug import (sensitive_post_parameters,
     10                                           sensitive_variables)
     11from django.utils.log import getLogger
    912
    1013from regressiontests.views import BrokenException, except_args
    1114
    def raises_template_does_not_exist(request):  
    128131        return render_to_response('i_dont_exist.html')
    129132    except TemplateDoesNotExist:
    130133        return technical_500_response(request, *sys.exc_info())
     134
     135def send_log(request, exc_info):
     136    logger = getLogger('django.request')
     137    logger.error('Internal Server Error: %s' % request.path,
     138        exc_info=exc_info,
     139        extra={
     140            'status_code': 500,
     141            'request': request
     142        }
     143    )
     144
     145def non_sensitive_view(request):
     146    # Do not just use plain strings for the variables' values in the code
     147    # so that the tests don't return false positives when the function's source
     148    # is displayed in the exception report.
     149    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
     150    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
     151    try:
     152        raise Exception
     153    except Exception:
     154        exc_info = sys.exc_info()
     155        send_log(request, exc_info)
     156        return technical_500_response(request, *exc_info)
     157   
     158@sensitive_variables('sauce')
     159@sensitive_post_parameters('bacon-key', 'sausage-key')
     160def sensitive_view(request):
     161    # Do not just use plain strings for the variables' values in the code
     162    # so that the tests don't return false positives when the function's source
     163    # is displayed in the exception report.
     164    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
     165    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
     166    try:
     167        raise Exception
     168    except Exception:
     169        exc_info = sys.exc_info()
     170        send_log(request, exc_info)
     171        return technical_500_response(request, *exc_info)
     172
     173@sensitive_variables()
     174@sensitive_post_parameters()
     175def paranoid_view(request):
     176    # Do not just use plain strings for the variables' values in the code
     177    # so that the tests don't return false positives when the function's source
     178    # is displayed in the exception report.
     179    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
     180    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
     181    try:
     182        raise Exception
     183    except Exception:
     184        exc_info = sys.exc_info()
     185        send_log(request, exc_info)
     186        return technical_500_response(request, *exc_info)
     187
     188class UnsafeExceptionReporterFilter(SafeExceptionReporterFilter):
     189    """
     190    Ignores all the filtering done by its parent class.
     191    """
     192   
     193    def get_post_parameters(self, request):
     194        return request.POST
     195           
     196    def get_traceback_frame_variables(self, request, tb_frame):
     197        return tb_frame.f_locals.items()
     198
     199
     200@sensitive_variables()
     201@sensitive_post_parameters()
     202def custom_exception_reporter_filter_view(request):
     203    # Do not just use plain strings for the variables' values in the code
     204    # so that the tests don't return false positives when the function's source
     205    # is displayed in the exception report.
     206    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
     207    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
     208    request.exception_reporter_filter = UnsafeExceptionReporterFilter()
     209    try:
     210        raise Exception
     211    except Exception:
     212        exc_info = sys.exc_info()
     213        send_log(request, exc_info)
     214        return technical_500_response(request, *exc_info)
Back to Top