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

File 14614.exception-reporter-filter.diff, 30.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..dc04e7c 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.security 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..75c4a13 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.security 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..738606b 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()
    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..1ea7f52 100644
    a b import os  
    33import re
    44import sys
    55import types
     6from pprint import pformat
    67
    78from django.conf import settings
    89from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
    from django.utils.encoding import smart_unicode, smart_str  
    1516
    1617HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD|PROFANITIES_LIST|SIGNATURE')
    1718
     19CLEANSED_SUBSTITUTE = u'********************'
     20
    1821def linebreak_iter(template_source):
    1922    yield 0
    2023    p = template_source.find('\n')
    def cleanse_setting(key, value):  
    3134    """
    3235    try:
    3336        if HIDDEN_SETTINGS.search(key):
    34             cleansed = '********************'
     37            cleansed = CLEANSED_SUBSTITUTE
    3538        else:
    3639            if isinstance(value, dict):
    3740                cleansed = dict((k, cleanse_setting(k, v)) for k,v in value.items())
    def technical_500_response(request, exc_type, exc_value, tb):  
    5962    html = reporter.get_traceback_html()
    6063    return HttpResponseServerError(html, mimetype='text/html')
    6164
     65# Cache for the default exception reporter filter class.
     66default_exception_reporter_filter = None
     67
     68def get_exception_reporter_filter(request):
     69    global default_exception_reporter_filter
     70    if default_exception_reporter_filter is None:
     71        # Load the default filter for the first time and cache it.
     72        modpath = settings.DEFAULT_EXCEPTION_REPORTER_FILTER
     73        modname, classname = modpath.rsplit('.', 1)
     74        try:
     75            mod = import_module(modname)
     76        except ImportError, e:
     77            raise ImproperlyConfigured(
     78            'Error importing default exception reporter filter %s: "%s"' % (modpath, e))
     79        try:
     80            default_exception_reporter_filter = getattr(mod, classname)
     81        except AttributeError:
     82            raise exceptions.ImproperlyConfigured('Default exception reporter filter module "%s" does not define a "%s" class' % (modname, classname))
     83    if request:
     84        filter = getattr(request, 'exception_reporter_filter', None)
     85        if filter is None:
     86            # The request hasn't been assigned a filter class yet, so assign it the default one.
     87            request.exception_reporter_filter = default_exception_reporter_filter
     88        return request.exception_reporter_filter(request)
     89    else:
     90        return default_exception_reporter_filter()
     91
     92class ExceptionReporterFilterBase(object):
     93    """
     94    Base for all exception reporter filter classes. All overridable hooks
     95    contain lenient default behaviours.
     96    """
     97    def __init__(self, request=None):
     98        self.request = request
     99   
     100    def show_request(self):
     101        return self.request is not None
     102   
     103    def get_request_repr(self):
     104        if self.show_request():
     105            # Mimic HttpRequest's own __repr__() except filter the POST parameters.
     106            return '<HttpRequest\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % \
     107                (pformat(self.request.GET),
     108                 pformat(self.get_filtered_post_dict()),
     109                 pformat(self.request.COOKIES),
     110                 pformat(self.request.META))
     111        else:
     112            return u''
     113   
     114    def show_source(self):
     115        return True
     116       
     117    def get_filtered_post_dict(self):
     118        if self.show_request():
     119            return self.request.POST
     120        else:
     121            return {}
     122   
     123    def show_traceback_frames(self):
     124        return True
     125   
     126    def show_traceback_frame(self, tb_frame):
     127        # Support for __traceback_hide__ which is used by a few libraries
     128        # to hide internal frames.
     129        return self.show_traceback_frames() and not tb_frame.f_locals.get('__traceback_hide__')
     130   
     131    def show_traceback_variables(self, tb_frame):
     132        return self.show_traceback_frame(tb_frame)
     133   
     134    def get_filtered_traceback_variables(self, tb_frame):
     135        if self.show_traceback_variables():
     136            return tb_frame.f_locals.items()
     137        else:
     138            return {}
     139
     140class SafeExceptionReporterFilter(ExceptionReporterFilterBase):
     141    """
     142    Use annotations made by the sensitive_post_parameters and
     143    sensitive_variables decorators to filter out sensitive information.
     144    """
     145    def get_filtered_post_dict(self):
     146        if not self.show_request():
     147            return {}
     148        else:
     149            sensitive_post_parameters = getattr(self.request, 'sensitive_post_parameters', [])
     150            if settings.DEBUG or not sensitive_post_parameters:
     151                return self.request.POST
     152            else:
     153                cleansed = self.request.POST.copy()
     154                if sensitive_post_parameters == '__EVERYTHING__':
     155                    # Cleanse all parameters
     156                    for k, v in cleansed.items():
     157                        cleansed[k] = CLEANSED_SUBSTITUTE
     158                    return cleansed
     159                else:
     160                    # Cleanse specified parameters
     161                    for param in sensitive_post_parameters:
     162                        if cleansed.has_key(param):
     163                            cleansed[param] = CLEANSED_SUBSTITUTE
     164                    return cleansed
     165   
     166    def get_filtered_traceback_variables(self, tb_frame):
     167        if not self.show_traceback_variables(tb_frame):
     168            return {}
     169        else:
     170            func_name = tb_frame.f_code.co_name
     171            func = tb_frame.f_globals.get(func_name)
     172            sensitive_variables = getattr(func, 'sensitive_variables', [])
     173            cleansed = []
     174            if settings.DEBUG or not sensitive_variables:
     175                # Potentially clean only the request if it's one of the frame variables.
     176                for name, value in tb_frame.f_locals.items():
     177                    if value == self.request:
     178                        value = self.get_request_repr()
     179                    cleansed.append((name, value))
     180                return cleansed
     181            else:
     182                if sensitive_variables == '__EVERYTHING__':
     183                    # Cleanse all variables
     184                    for name, value in tb_frame.f_locals.items():
     185                        cleansed.append((name, CLEANSED_SUBSTITUTE))
     186                    return cleansed
     187                else:
     188                    # Cleanse specified variables
     189                    for name, value in tb_frame.f_locals.items():
     190                        if name in sensitive_variables:
     191                            value = CLEANSED_SUBSTITUTE
     192                        elif value == self.request:
     193                            # Clean the request's POST parameters.
     194                            value = self.get_request_repr()
     195                        cleansed.append((name, value))
     196                    return cleansed
     197   
    62198class ExceptionReporter(object):
    63199    """
    64200    A class to organize and coordinate reporting on exceptions.
    65201    """
    66202    def __init__(self, request, exc_type, exc_value, tb, is_email=False):
    67203        self.request = request
     204        self.exception_reporter_filter = get_exception_reporter_filter(request)
    68205        self.exc_type = exc_type
    69206        self.exc_value = exc_value
    70207        self.tb = tb
    class ExceptionReporter(object):  
    123260            'is_email': self.is_email,
    124261            'unicode_hint': unicode_hint,
    125262            'frames': frames,
    126             'request': self.request,
     263            'request': self.request if self.exception_reporter_filter.show_request() else None,
     264            'filtered_POST': self.exception_reporter_filter.get_filtered_post_dict(),
    127265            'settings': get_safe_settings(),
    128266            'sys_executable': sys.executable,
    129267            'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
    class ExceptionReporter(object):  
    142280        if frames:
    143281            c['lastframe'] = frames[-1]
    144282        return t.render(c)
    145 
     283   
    146284    def get_template_exception_info(self):
    147285        origin, (start, end) = self.exc_value.source
    148286        template_source = origin.reload()
    class ExceptionReporter(object):  
    219357        return lower_bound, pre_context, context_line, post_context
    220358
    221359    def get_traceback_frames(self):
     360        if not self.exception_reporter_filter.show_traceback_frames():
     361            return []
    222362        frames = []
    223363        tb = self.tb
    224364        while tb is not None:
    225             # support for __traceback_hide__ which is used by a few libraries
    226             # to hide internal frames.
    227             if tb.tb_frame.f_locals.get('__traceback_hide__'):
     365            if not self.exception_reporter_filter.show_traceback_frame(tb.tb_frame):
    228366                tb = tb.tb_next
    229367                continue
    230368            filename = tb.tb_frame.f_code.co_filename
    class ExceptionReporter(object):  
    232370            lineno = tb.tb_lineno - 1
    233371            loader = tb.tb_frame.f_globals.get('__loader__')
    234372            module_name = tb.tb_frame.f_globals.get('__name__')
    235             pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(filename, lineno, 7, loader, module_name)
     373            if self.exception_reporter_filter.show_source():
     374                pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(filename, lineno, 7, loader, module_name)
     375            else:
     376                pre_context_lineno, pre_context, context_line, post_context = '', '', '', ''
    236377            if pre_context_lineno is not None:
    237378                frames.append({
    238379                    'tb': tb,
    239380                    'filename': filename,
    240381                    'function': function,
    241382                    'lineno': lineno + 1,
    242                     'vars': tb.tb_frame.f_locals.items(),
     383                    'vars': self.exception_reporter_filter.get_filtered_traceback_variables(tb.tb_frame),
    243384                    'id': id(tb),
    244385                    'pre_context': pre_context,
    245386                    'context_line': context_line,
    Exception Value: {{ exception_value|force_escape }}  
    643784  {% endif %}
    644785
    645786  <h3 id="post-info">POST</h3>
    646   {% if request.POST %}
     787  {% if filtered_POST %}
    647788    <table class="req">
    648789      <thead>
    649790        <tr>
    Exception Value: {{ exception_value|force_escape }}  
    652793        </tr>
    653794      </thead>
    654795      <tbody>
    655         {% for var in request.POST.items %}
     796        {% for var in filtered_POST.items %}
    656797          <tr>
    657798            <td>{{ var.0 }}</td>
    658799            <td class="code"><pre>{{ var.1|pprint }}</pre></td>
  • new file django/views/decorators/security.py

    diff --git a/django/views/decorators/security.py b/django/views/decorators/security.py
    new file mode 100644
    index 0000000..8c92ec7
    - +  
     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('password', 'credit_card')
     15        def my_function()
     16            password = 'foo'
     17            credit_card = '5555 5555 5555 5555'
     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 = '__EVERYTHING__'
     34            return func(*args, **kwargs)
     35        return wrapper
     36    if len(variables) == 1 and callable(variables[0]):
     37        # This is of the form: func = sensitive_variables(func)
     38        func = variables[0]
     39        variables = []
     40        return decorator(func)
     41    else:
     42        # This is of the form: func = sensitive_variables(...)(func)
     43        return decorator
     44
     45
     46def sensitive_post_parameters(*params):
     47    """
     48    Indicates which POST parameters used in the decorated view are sensitive,
     49    so that those parameters can later be treated in a special way, for example
     50    by hiding them when logging unhandled exceptions.
     51   
     52    Two forms are accepted:
     53   
     54    * with specified parameters:
     55   
     56        @sensitive_post_parameters('password', 'credit_card')
     57        def my_view(request)
     58            pw = request.POST['password']
     59            cc = request.POST['credit_card']
     60            ...
     61           
     62    * without any specified parameters, in which case it is assumed that
     63      all parameters are considered sensitive:
     64     
     65        @sensitive_post_parameters
     66        def my_view(request)
     67            ...
     68    """
     69    def decorator(view):
     70        @functools.wraps(view)
     71        def wrapper(request, *args, **kwargs):
     72            if params:
     73                request.sensitive_post_parameters = params
     74            else:
     75                request.sensitive_post_parameters = '__EVERYTHING__'
     76            return view(request, *args, **kwargs)
     77        return wrapper
     78    if len(params) == 1 and callable(params[0]):
     79        # This is of the form: view = sensitive_post_parameters(view)
     80        view = params[0]
     81        params = []
     82        return decorator(view)
     83    else:
     84        # This is of the form: view = sensitive_post_parameters(...)(view)
     85        return decorator
  • 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..7f274c3 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.security 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