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

File 14614.exception-reporter-filter.2.diff, 40.9 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..9ee1cd9 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 class.
     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        filter = getattr(request, 'exception_reporter_filter', default_exception_reporter_filter)
     86        return filter()
     87    else:
     88        return default_exception_reporter_filter()
     89
     90class ExceptionReporterFilter(object):
     91    """
     92    Base for all exception reporter filter classes. All overridable hooks
     93    contain lenient default behaviours.
     94    """
     95   
     96    def get_request_repr(self, request):
     97        if request is None:
     98            return repr(None)
     99        else:
     100            # Mimic the request's (WSGIRequest, ModPythonRequest or
     101            # HttpRequest) own __repr__() method and and filter out the POST
     102            # parameters if necessary.
     103            _repr = re.sub(r'(.*)\nGET:(.*),\nPOST:(.*),\nCOOKIES:([.\n]*)',
     104                   r'\1\nGET:\2,\nPOST:%s,\nCOOKIES:\4' % self.get_post_parameters(request),
     105                   repr(request)
     106                )
     107            return _repr
     108
     109    def get_post_parameters(self, request):
     110        if request is None:
     111            return {}
     112        else:
     113            return request.POST
     114           
     115    def get_traceback_frame_variables(self, request, tb_frame):
     116        return tb_frame.f_locals.items()
     117
     118class SafeExceptionReporterFilter(ExceptionReporterFilter):
     119    """
     120    Use annotations made by the sensitive_post_parameters and
     121    sensitive_variables decorators to filter out sensitive information.
     122    """
     123   
     124    def _is_active(self, request):
     125        """
     126        This filter is to add safety in production environments (i.e. DEBUG
     127        is False). If DEBUG is True then your site is not safe anyway.
     128        This hook is provided as a convenience to easily activate or
     129        deactivate the filter on a per request basis.
     130        """
     131        return settings.DEBUG is False
     132   
     133    def get_post_parameters(self, request):
     134        """
     135        Replaces the values of POST parameters marked as sensitive with
     136        stars (*********).
     137        """
     138        if request is None:
     139            return {}
     140        else:
     141            sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
     142            if self._is_active(request) and sensitive_post_parameters:
     143                cleansed = request.POST.copy()
     144                if sensitive_post_parameters == '__ALL__':
     145                    # Cleanse all parameters.
     146                    for k, v in cleansed.items():
     147                        cleansed[k] = CLEANSED_SUBSTITUTE
     148                    return cleansed
     149                else:
     150                    # Cleanse only the specified parameters.
     151                    for param in sensitive_post_parameters:
     152                        if cleansed.has_key(param):
     153                            cleansed[param] = CLEANSED_SUBSTITUTE
     154                    return cleansed
     155            else:
     156                return request.POST
     157   
     158    def get_traceback_frame_variables(self, request, tb_frame):
     159        """
     160        Replaces the values of variables marked as sensitive with
     161        stars (*********).
     162        """
     163        func_name = tb_frame.f_code.co_name
     164        func = tb_frame.f_globals.get(func_name)
     165        sensitive_variables = getattr(func, 'sensitive_variables', [])
     166        cleansed = []
     167        if self._is_active(request) and sensitive_variables:
     168            if sensitive_variables == '__ALL__':
     169                # Cleanse all variables
     170                for name, value in tb_frame.f_locals.items():
     171                    cleansed.append((name, CLEANSED_SUBSTITUTE))
     172                return cleansed
     173            else:
     174                # Cleanse specified variables
     175                for name, value in tb_frame.f_locals.items():
     176                    if name in sensitive_variables:
     177                        value = CLEANSED_SUBSTITUTE
     178                    elif isinstance(value, HttpRequest):
     179                        # Cleanse the request's POST parameters.
     180                        value = self.get_request_repr(value)
     181                    cleansed.append((name, value))
     182                return cleansed
     183        else:
     184            # Potentially cleanse only the request if it's one of the frame variables.
     185            for name, value in tb_frame.f_locals.items():
     186                if isinstance(value, HttpRequest):
     187                    # Cleanse the request's POST parameters.
     188                    value = self.get_request_repr(value)
     189                cleansed.append((name, value))
     190            return cleansed
     191   
    62192class ExceptionReporter(object):
    63193    """
    64194    A class to organize and coordinate reporting on exceptions.
    65195    """
    66196    def __init__(self, request, exc_type, exc_value, tb, is_email=False):
    67197        self.request = request
     198        self.filter = get_exception_reporter_filter(self.request)
    68199        self.exc_type = exc_type
    69200        self.exc_value = exc_value
    70201        self.tb = tb
    class ExceptionReporter(object):  
    124255            'unicode_hint': unicode_hint,
    125256            'frames': frames,
    126257            'request': self.request,
     258            'filtered_POST': self.filter.get_post_parameters(self.request),
    127259            'settings': get_safe_settings(),
    128260            'sys_executable': sys.executable,
    129261            'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
    class ExceptionReporter(object):  
    222354        frames = []
    223355        tb = self.tb
    224356        while tb is not None:
    225             # support for __traceback_hide__ which is used by a few libraries
     357            # Support for __traceback_hide__ which is used by a few libraries
    226358            # to hide internal frames.
    227359            if tb.tb_frame.f_locals.get('__traceback_hide__'):
    228360                tb = tb.tb_next
    class ExceptionReporter(object):  
    239371                    'filename': filename,
    240372                    'function': function,
    241373                    'lineno': lineno + 1,
    242                     'vars': tb.tb_frame.f_locals.items(),
     374                    'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame),
    243375                    'id': id(tb),
    244376                    'pre_context': pre_context,
    245377                    'context_line': context_line,
    class ExceptionReporter(object):  
    247379                    'pre_context_lineno': pre_context_lineno + 1,
    248380                })
    249381            tb = tb.tb_next
    250 
    251382        return frames
    252383
    253384    def format_exception(self):
    Exception Value: {{ exception_value|force_escape }}  
    643774  {% endif %}
    644775
    645776  <h3 id="post-info">POST</h3>
    646   {% if request.POST %}
     777  {% if filtered_POST %}
    647778    <table class="req">
    648779      <thead>
    649780        <tr>
    Exception Value: {{ exception_value|force_escape }}  
    652783        </tr>
    653784      </thead>
    654785      <tbody>
    655         {% for var in request.POST.items %}
     786        {% for var in filtered_POST.items %}
    656787          <tr>
    657788            <td>{{ var.0 }}</td>
    658789            <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..04a2343 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, you need to define your own filter class and
     233tell Django to use it via the :setting:`DEFAULT_EXCEPTION_REPORTER_FILTER`
     234setting:
     235
     236    .. code-block:: python
     237   
     238        DEFAULT_EXCEPTION_REPORTER_FILTER = 'path.to.your.CustomExceptionReporterFilter'
     239   
     240Your custom filter class needs to inherit from
     241:class:`django.views.debug.ExceptionReporterFilter` and may override the
     242following methods.
     243
     244.. class:: django.views.debug.ExceptionReporterFilter
     245   
     246.. method:: ExceptionReporterFilter.get_request_repr(self, request)
     247
     248    Returns the representation string of the request object, that is, the
     249    value that would be returned by ``repr(request)``, except it filters
     250    out the POST parameters as determined by
     251    :meth:`ExceptionReporterFilter.get_post_parameters`.
     252
     253.. method:: ExceptionReporterFilter.get_post_parameters(self, request)
     254
     255    Returns the filtered dictionary of POST parameters. For example,
     256    :class:`django.views.debug.SafeExceptionReporterFilter` replaces the
     257    values of sensitive parameters with stars (`**********`).
     258   
     259.. method:: ExceptionReporterFilter.get_traceback_frame_variables(self, request, tb_frame)
     260
     261    Returns the filtered dictionary of local variables for the given traceback
     262    frame. For example,
     263    :class:`django.views.debug.SafeExceptionReporterFilter` replaces the
     264    values of sensitive variables with stars (`**********`).
     265
     266.. seealso::
     267
     268    You can also set up custom error reporting by writing a custom piece of
     269    :ref:`exception middleware <exception-middleware>`. If you do write custom
     270    error handling, it's a good idea to emulate Django's built-in error handling
     271    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..44186b6 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)
    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    rf = RequestFactory()
     155    breakfast_data = {'sausage-key': 'sausage-value',
     156                      'baked-beans-key': 'baked-beans-value',
     157                      'hash-brown-key': 'hash-brown-value',
     158                      'bacon-key': 'bacon-value',}
     159           
     160    def verify_unsafe_response(self, view):
     161        """
     162        Asserts that potentially sensitive info are displayed in the response.
     163        """
     164        request = self.rf.post('/some_url/', self.breakfast_data)
     165        response = view(request)
     166        # All variables are shown.
     167        self.assertContains(response, 'cooked_eggs', status_code=500)
     168        self.assertContains(response, 'scrambled', status_code=500)
     169        self.assertContains(response, 'sauce', status_code=500)
     170        self.assertContains(response, 'worcestershire', status_code=500)
     171        for k, v in self.breakfast_data.items():
     172            # All POST parameters are shown.
     173            self.assertContains(response, k, status_code=500)
     174            self.assertContains(response, v, status_code=500)
     175   
     176    def verify_safe_response(self, view):
     177        """
     178        Asserts that certain sensitive info are not displayed in the response.
     179        """
     180        request = self.rf.post('/some_url/', self.breakfast_data)
     181        response = view(request)
     182        # Non-sensitive variable's name and value are shown.
     183        self.assertContains(response, 'cooked_eggs', status_code=500)
     184        self.assertContains(response, 'scrambled', status_code=500)
     185        # Sensitive variable's name is shown but not its value.
     186        self.assertContains(response, 'sauce', status_code=500)
     187        self.assertNotContains(response, 'worcestershire', status_code=500)
     188        for k, v in self.breakfast_data.items():
     189            # All POST parameters' names are shown.
     190            self.assertContains(response, k, status_code=500)
     191        # Non-sensitive POST parameters' values are shown.
     192        self.assertContains(response, 'baked-beans-value', status_code=500)
     193        self.assertContains(response, 'hash-brown-value', status_code=500)
     194        # Sensitive POST parameters' values are not shown.
     195        self.assertNotContains(response, 'sausage-value', status_code=500)
     196        self.assertNotContains(response, 'bacon-value', status_code=500)
     197   
     198    def verify_paranoid_response(self, view):
     199        """
     200        Asserts that no variables or POST parameters are displayed in the response.
     201        """
     202        request = self.rf.post('/some_url/', self.breakfast_data)
     203        response = view(request)
     204        # Show variable names but not their values.
     205        self.assertContains(response, 'cooked_eggs', status_code=500)
     206        self.assertNotContains(response, 'scrambled', status_code=500)
     207        self.assertContains(response, 'sauce', status_code=500)
     208        self.assertNotContains(response, 'worcestershire', status_code=500)
     209        for k, v in self.breakfast_data.items():
     210            # All POST parameters' names are shown.
     211            self.assertContains(response, k, status_code=500)
     212            # No POST parameters' values are shown.
     213            self.assertNotContains(response, v, status_code=500)
     214       
     215    def verify_unsafe_email(self, view):
     216        """
     217        Asserts that potentially sensitive info are displayed in the email report.
     218        """
     219        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     220            mail.outbox = [] # Empty outbox
     221            request = self.rf.post('/some_url/', self.breakfast_data)
     222            response = view(request)
     223            self.assertEquals(len(mail.outbox), 1)
     224            email = mail.outbox[0]
     225            # Frames vars are never shown in plain text email reports.
     226            self.assertNotIn('cooked_eggs', email.body)
     227            self.assertNotIn('scrambled', email.body)
     228            self.assertNotIn('sauce', email.body)
     229            self.assertNotIn('worcestershire', email.body)
     230            for k, v in self.breakfast_data.items():
     231                # All POST parameters are shown.
     232                self.assertIn(k, email.body)
     233                self.assertIn(v, email.body)
     234       
     235    def verify_safe_email(self, view):
     236        """
     237        Asserts that certain sensitive info are not displayed in the email report.
     238        """
     239        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     240            mail.outbox = [] # Empty outbox
     241            request = self.rf.post('/some_url/', self.breakfast_data)
     242            response = view(request)
     243            self.assertEquals(len(mail.outbox), 1)
     244            email = mail.outbox[0]
     245            # Frames vars are never shown in plain text email reports.
     246            self.assertNotIn('cooked_eggs', email.body)
     247            self.assertNotIn('scrambled', email.body)
     248            self.assertNotIn('sauce', email.body)
     249            self.assertNotIn('worcestershire', email.body)
     250            for k, v in self.breakfast_data.items():
     251                # All POST parameters' names are shown.
     252                self.assertIn(k, email.body)
     253            # Non-sensitive POST parameters' values are shown.
     254            self.assertIn('baked-beans-value', email.body)
     255            self.assertIn('hash-brown-value', email.body)
     256            # Sensitive POST parameters' values are not shown.
     257            self.assertNotIn('sausage-value', email.body)
     258            self.assertNotIn('bacon-value', email.body)
     259   
     260    def verify_paranoid_email(self, view):
     261        """
     262        Asserts that no variables or POST parameters are displayed in the email report.
     263        """
     264        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     265            mail.outbox = [] # Empty outbox
     266            request = self.rf.post('/some_url/', self.breakfast_data)
     267            response = view(request)
     268            self.assertEquals(len(mail.outbox), 1)
     269            email = mail.outbox[0]
     270            # Frames vars are never shown in plain text email reports.
     271            self.assertNotIn('cooked_eggs', email.body)
     272            self.assertNotIn('scrambled', email.body)
     273            self.assertNotIn('sauce', email.body)
     274            self.assertNotIn('worcestershire', email.body)
     275            for k, v in self.breakfast_data.items():
     276                # All POST parameters' names are shown.
     277                self.assertIn(k, email.body)
     278                # No POST parameters' values are shown.
     279                self.assertNotIn(v, email.body)
     280           
     281    def test_non_sensitive_request(self):
     282        """
     283        Ensure that everything (request info and frame variables) can bee seen
     284        in the default error reports for non-sensitive requests.
     285        Refs #14614.
     286        """
     287        with self.settings(DEBUG=True):
     288            self.verify_unsafe_response(non_sensitive_view)
     289            self.verify_unsafe_email(non_sensitive_view)
     290               
     291        with self.settings(DEBUG=False):
     292            self.verify_unsafe_response(non_sensitive_view)
     293            self.verify_unsafe_email(non_sensitive_view)
     294       
     295    def test_sensitive_request(self):
     296        """
     297        Ensure that sensitive POST parameters and frame variables cannot be
     298        seen in the default error reports for sensitive requests.
     299        Refs #14614.
     300        """
     301        with self.settings(DEBUG=True):
     302            self.verify_unsafe_response(sensitive_view)
     303            self.verify_unsafe_email(sensitive_view)
     304           
     305        with self.settings(DEBUG=False):
     306            self.verify_safe_response(sensitive_view)
     307            self.verify_safe_email(sensitive_view)
     308   
     309    def test_paranoid_request(self):
     310        """
     311        Ensure that no POST parameters and frame variables can be seen in the
     312        default error reports for "paranoid" requests.
     313        Refs #14614.
     314        """
     315        with self.settings(DEBUG=True):
     316            self.verify_unsafe_response(paranoid_view)
     317            self.verify_unsafe_email(paranoid_view)
     318           
     319        with self.settings(DEBUG=False):
     320            self.verify_paranoid_response(paranoid_view)
     321            self.verify_paranoid_email(paranoid_view)
  • tests/regressiontests/views/views.py

    diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py
    index 11d289f..29da20f 100644
    a b from django.core.urlresolvers import get_resolver  
    66from django.shortcuts import render_to_response, render
    77from django.template import Context, RequestContext, TemplateDoesNotExist
    88from django.views.debug import technical_500_response
     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)
Back to Top