Ticket #10841: 10841.4.diff

File 10841.4.diff, 16.7 KB (added by Ramiro Morales, 8 years ago)

Patch updated to account for new 1.4 sensitive POST vars filtering + corresponding tests. Thanks Julien

  • django/views/debug.py

    diff --git a/django/views/debug.py b/django/views/debug.py
    a b  
    5959    the values returned from sys.exc_info() and friends.
    6060    """
    6161    reporter = ExceptionReporter(request, exc_type, exc_value, tb)
    62     html = reporter.get_traceback_html()
    63     return HttpResponseServerError(html, mimetype='text/html')
     62    if request.is_ajax():
     63        text = reporter.get_traceback_text()
     64        return HttpResponseServerError(text, mimetype='text/plain')
     65    else:
     66        html = reporter.get_traceback_html()
     67        return HttpResponseServerError(html, mimetype='text/html')
    6468
    6569# Cache for the default exception reporter filter instance.
    6670default_exception_reporter_filter = None
     
    201205            self.exc_value = Exception('Deprecated String Exception: %r' % self.exc_type)
    202206            self.exc_type = type(self.exc_value)
    203207
    204     def get_traceback_html(self):
    205         "Return HTML code for traceback."
     208    def get_traceback_data(self):
     209        "Return a Context instance containing traceback information."
    206210
    207211        if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
    208212            from django.template.loader import template_source_loaders
     
    240244                unicode_str = self.exc_value.args[1]
    241245                unicode_hint = smart_unicode(unicode_str[max(start-5, 0):min(end+5, len(unicode_str))], 'ascii', errors='replace')
    242246        from django import get_version
    243         t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
    244         c = Context({
     247        c = {
    245248            'is_email': self.is_email,
    246249            'unicode_hint': unicode_hint,
    247250            'frames': frames,
     
    256259            'template_info': self.template_info,
    257260            'template_does_not_exist': self.template_does_not_exist,
    258261            'loader_debug_info': self.loader_debug_info,
    259         })
     262        }
    260263        # Check whether exception info is available
    261264        if self.exc_type:
    262265            c['exception_type'] = self.exc_type.__name__
     
    264267            c['exception_value'] = smart_unicode(self.exc_value, errors='replace')
    265268        if frames:
    266269            c['lastframe'] = frames[-1]
     270        return c
     271
     272    def get_traceback_html(self):
     273        "Return HTML version of debug 500 HTTP error page."
     274        t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
     275        c = Context(self.get_traceback_data())
     276        return t.render(c)
     277
     278    def get_traceback_text(self):
     279        "Return plain text version of debug 500 HTTP error page."
     280        t = Template(TECHNICAL_500_TEXT_TEMPLATE, name='Technical 500 template')
     281        c = Context(self.get_traceback_data(), autoescape=False)
    267282        return t.render(c)
    268283
    269284    def get_template_exception_info(self):
     
    890905</html>
    891906"""
    892907
     908TECHNICAL_500_TEXT_TEMPLATE = """{% firstof exception_type 'Report' %}{% if request %} at {{ request.path_info }}{% endif %}
     909{% firstof exception_value 'No exception supplied' %}
     910{% if request %}
     911Request Method: {{ request.META.REQUEST_METHOD }}
     912Request URL: {{ request.build_absolute_uri }}{% endif %}
     913Django Version: {{ django_version_info }}
     914Python Executable: {{ sys_executable }}
     915Python Version: {{ sys_version_info }}
     916Python Path: {{ sys_path }}
     917Server time: {{server_time|date:"r"}}
     918Installed Applications:
     919{{ settings.INSTALLED_APPS|pprint }}
     920Installed Middleware:
     921{{ settings.MIDDLEWARE_CLASSES|pprint }}
     922{% if template_does_not_exist %}Template loader Error:
     923{% if loader_debug_info %}Django tried loading these templates, in this order:
     924{% for loader in loader_debug_info %}Using loader {{ loader.loader }}:
     925{% for t in loader.templates %}{{ t.name }} (File {% if t.exists %}exists{% else %}does not exist{% endif %})
     926{% endfor %}{% endfor %}
     927{% else %}Django couldn't find any templates because your TEMPLATE_LOADERS setting is empty!
     928{% endif %}
     929{% endif %}{% if template_info %}
     930Template error:
     931In template {{ template_info.name }}, error at line {{ template_info.line }}
     932   {{ template_info.message }}{% for source_line in template_info.source_lines %}{% ifequal source_line.0 template_info.line %}
     933   {{ source_line.0 }} : {{ template_info.before }} {{ template_info.during }} {{ template_info.after }}
     934{% else %}
     935   {{ source_line.0 }} : {{ source_line.1 }}
     936   {% endifequal %}{% endfor %}{% endif %}{% if frames %}
     937Traceback:
     938{% for frame in frames %}File "{{ frame.filename }}" in {{ frame.function }}
     939{% if frame.context_line %}  {{ frame.lineno }}. {{ frame.context_line }}{% endif %}
     940{% endfor %}
     941{% if exception_type %}Exception Type: {{ exception_type }}{% if request %} at {{ request.path_info }}{% endif %}
     942{% if exception_value %}Exception Value: {{ exception_value }}{% endif %}{% endif %}{% endif %}
     943{% if request %}Request information:
     944GET:{% for k, v in request.GET.items %}
     945{{ k }} = {{ v|stringformat:"r" }}{% empty %} No GET data{% endfor %}
     946
     947POST:{% for k, v in filtered_POST.items %}
     948{{ k }} = {{ v|stringformat:"r" }}{% empty %} No POST data{% endfor %}
     949
     950FILES:{% for k, v in request.FILES.items %}
     951{{ k }} = {{ v|stringformat:"r" }}{% empty %} No FILES data{% endfor %}
     952
     953COOKIES:{% for k, v in request.COOKIES.items %}
     954{{ k }} = {{ v|stringformat:"r" }}{% empty %} No cookie data{% endfor %}
     955
     956META:{% for k, v in request.META.items|dictsort:"0" %}
     957{{ k }} = {{ v|stringformat:"r" }}{% endfor %}
     958{% else %}Request data not supplied
     959{% endif %}
     960Settings:
     961Using settings module {{ settings.SETTINGS_MODULE }}{% for k, v in settings.items|dictsort:"0" %}
     962{{ k }} = {{ v|stringformat:"r" }}{% endfor %}
     963
     964You're seeing this error because you have DEBUG = True in your
     965Django settings file. Change that to False, and Django will
     966display a standard 500 page.
     967"""
     968
    893969TECHNICAL_404_TEMPLATE = """
    894970<!DOCTYPE html>
    895971<html lang="en">
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    a b  
    315315  be able to retrieve a translation string without displaying it but setting
    316316  a template context variable instead.
    317317
     318* A new plaintext version of the HTTP 500 status code internal error page
     319  served when :setting:`DEBUG` is ``True`` is now sent to the client when
     320  Django detect that the request has originated in JavaScript code
     321  (:meth:`~django.http.HttpRequest.is_ajax` is used for this).
     322
     323  Similarly to its HTML counterpart, it contains a collection of different
     324  pieces of useful information about the state of the web application.
     325
     326  This should make it easier to read when debugging interaction with
     327  client-side Javascript code.
     328
    318329.. _backwards-incompatible-changes-1.4:
    319330
    320331Backwards incompatible changes in 1.4
  • tests/regressiontests/views/tests/debug.py

    diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py
    a b  
    171171        self.assertIn('<p>Request data not supplied</p>', html)
    172172
    173173
    174 class ExceptionReporterFilterTests(TestCase):
    175     """
    176     Ensure that sensitive information can be filtered out of error reports.
    177     Refs #14614.
    178     """
     174class PlainTextReportTests(TestCase):
    179175    rf = RequestFactory()
     176
     177    def test_request_and_exception(self):
     178        "A simple exception report can be generated"
     179        try:
     180            request = self.rf.get('/test_view/')
     181            raise ValueError("Can't find my keys")
     182        except ValueError:
     183            exc_type, exc_value, tb = sys.exc_info()
     184        reporter = ExceptionReporter(request, exc_type, exc_value, tb)
     185        text = reporter.get_traceback_text()
     186        self.assertIn('ValueError at /test_view/', text)
     187        self.assertIn("Can't find my keys", text)
     188        self.assertIn('Request Method:', text)
     189        self.assertIn('Request URL:', text)
     190        self.assertIn('Exception Type:', text)
     191        self.assertIn('Exception Value:', text)
     192        self.assertIn('Traceback:', text)
     193        self.assertIn('Request information:', text)
     194        self.assertNotIn('Request data not supplied', text)
     195
     196    def test_no_request(self):
     197        "An exception report can be generated without request"
     198        try:
     199            raise ValueError("Can't find my keys")
     200        except ValueError:
     201            exc_type, exc_value, tb = sys.exc_info()
     202        reporter = ExceptionReporter(None, exc_type, exc_value, tb)
     203        text = reporter.get_traceback_text()
     204        self.assertIn('ValueError', text)
     205        self.assertIn("Can't find my keys", text)
     206        self.assertNotIn('Request Method:', text)
     207        self.assertNotIn('Request URL:', text)
     208        self.assertIn('Exception Type:', text)
     209        self.assertIn('Exception Value:', text)
     210        self.assertIn('Traceback:', text)
     211        self.assertIn('Request data not supplied', text)
     212
     213    def test_no_exception(self):
     214        "An exception report can be generated for just a request"
     215        request = self.rf.get('/test_view/')
     216        reporter = ExceptionReporter(request, None, None, None)
     217        text = reporter.get_traceback_text()
     218
     219    def test_request_and_message(self):
     220        "A message can be provided in addition to a request"
     221        request = self.rf.get('/test_view/')
     222        reporter = ExceptionReporter(request, None, "I'm a little teapot", None)
     223        text = reporter.get_traceback_text()
     224
     225    def test_message_only(self):
     226        reporter = ExceptionReporter(None, None, "I'm a little teapot", None)
     227        text = reporter.get_traceback_text()
     228
     229
     230class ExceptionReportTestMixin(object):
     231
     232    # Mixin used in the ExceptionReporterFilterTests and
     233    # AjaxResponseExceptionReporterFilter tests below
     234
    180235    breakfast_data = {'sausage-key': 'sausage-value',
    181236                      'baked-beans-key': 'baked-beans-value',
    182237                      'hash-brown-key': 'hash-brown-value',
    183238                      'bacon-key': 'bacon-value',}
    184239
    185     def verify_unsafe_response(self, view):
     240    def verify_unsafe_response(self, view, check_for_vars=True):
    186241        """
    187242        Asserts that potentially sensitive info are displayed in the response.
    188243        """
    189244        request = self.rf.post('/some_url/', self.breakfast_data)
    190245        response = view(request)
    191         # All variables are shown.
    192         self.assertContains(response, 'cooked_eggs', status_code=500)
    193         self.assertContains(response, 'scrambled', status_code=500)
    194         self.assertContains(response, 'sauce', status_code=500)
    195         self.assertContains(response, 'worcestershire', status_code=500)
     246        if check_for_vars:
     247            # All variables are shown.
     248            self.assertContains(response, 'cooked_eggs', status_code=500)
     249            self.assertContains(response, 'scrambled', status_code=500)
     250            self.assertContains(response, 'sauce', status_code=500)
     251            self.assertContains(response, 'worcestershire', status_code=500)
     252
    196253        for k, v in self.breakfast_data.items():
    197254            # All POST parameters are shown.
    198255            self.assertContains(response, k, status_code=500)
    199256            self.assertContains(response, v, status_code=500)
    200257
    201     def verify_safe_response(self, view):
     258    def verify_safe_response(self, view, check_for_vars=True):
    202259        """
    203260        Asserts that certain sensitive info are not displayed in the response.
    204261        """
    205262        request = self.rf.post('/some_url/', self.breakfast_data)
    206263        response = view(request)
    207         # Non-sensitive variable's name and value are shown.
    208         self.assertContains(response, 'cooked_eggs', status_code=500)
    209         self.assertContains(response, 'scrambled', status_code=500)
    210         # Sensitive variable's name is shown but not its value.
    211         self.assertContains(response, 'sauce', status_code=500)
    212         self.assertNotContains(response, 'worcestershire', status_code=500)
     264        if check_for_vars:
     265            # Non-sensitive variable's name and value are shown.
     266            self.assertContains(response, 'cooked_eggs', status_code=500)
     267            self.assertContains(response, 'scrambled', status_code=500)
     268            # Sensitive variable's name is shown but not its value.
     269            self.assertContains(response, 'sauce', status_code=500)
     270            self.assertNotContains(response, 'worcestershire', status_code=500)
     271
    213272        for k, v in self.breakfast_data.items():
    214273            # All POST parameters' names are shown.
    215274            self.assertContains(response, k, status_code=500)
     
    220279        self.assertNotContains(response, 'sausage-value', status_code=500)
    221280        self.assertNotContains(response, 'bacon-value', status_code=500)
    222281
    223     def verify_paranoid_response(self, view):
     282    def verify_paranoid_response(self, view, check_for_vars=True):
    224283        """
    225284        Asserts that no variables or POST parameters are displayed in the response.
    226285        """
    227286        request = self.rf.post('/some_url/', self.breakfast_data)
    228287        response = view(request)
    229         # Show variable names but not their values.
    230         self.assertContains(response, 'cooked_eggs', status_code=500)
    231         self.assertNotContains(response, 'scrambled', status_code=500)
    232         self.assertContains(response, 'sauce', status_code=500)
    233         self.assertNotContains(response, 'worcestershire', status_code=500)
     288        if check_for_vars:
     289            # Show variable names but not their values.
     290            self.assertContains(response, 'cooked_eggs', status_code=500)
     291            self.assertNotContains(response, 'scrambled', status_code=500)
     292            self.assertContains(response, 'sauce', status_code=500)
     293            self.assertNotContains(response, 'worcestershire', status_code=500)
     294
    234295        for k, v in self.breakfast_data.items():
    235296            # All POST parameters' names are shown.
    236297            self.assertContains(response, k, status_code=500)
     
    303364                # No POST parameters' values are shown.
    304365                self.assertNotIn(v, email.body)
    305366
     367
     368class ExceptionReporterFilterTests(TestCase, ExceptionReportTestMixin):
     369    """
     370    Ensure that sensitive information can be filtered out of error reports.
     371    Refs #14614.
     372    """
     373    rf = RequestFactory()
     374
    306375    def test_non_sensitive_request(self):
    307376        """
    308377        Ensure that everything (request info and frame variables) can bee seen
     
    354423        with self.settings(DEBUG=False):
    355424            self.verify_unsafe_response(custom_exception_reporter_filter_view)
    356425            self.verify_unsafe_email(custom_exception_reporter_filter_view)
     426
     427
     428class AjaxResponseExceptionReporterFilter(TestCase, ExceptionReportTestMixin):
     429    """
     430    Ensure that sensitive information can be filtered out of error reports.
     431
     432    Here we specifically test the plain text 500 error page generated when it
     433    has been detected the request was sent by JS code. We don't check
     434    (non)existence of views variables in the traceback information section of
     435    the response content because they aren't included in these error pages.
     436    Refs #14614.
     437    """
     438    rf = RequestFactory(HTTP_X_REQUESTED_WITH='XMLHttpRequest')
     439
     440    def test_non_sensitive_request(self):
     441        """
     442        Ensure that request info can bee seen in the default error reports for
     443        non-sensitive requests.
     444        """
     445        with self.settings(DEBUG=True):
     446            self.verify_unsafe_response(non_sensitive_view, check_for_vars=False)
     447
     448        with self.settings(DEBUG=False):
     449            self.verify_unsafe_response(non_sensitive_view, check_for_vars=False)
     450
     451    def test_sensitive_request(self):
     452        """
     453        Ensure that sensitive POST parameters cannot be seen in the default
     454        error reports for sensitive requests.
     455        """
     456        with self.settings(DEBUG=True):
     457            self.verify_unsafe_response(sensitive_view, check_for_vars=False)
     458
     459        with self.settings(DEBUG=False):
     460            self.verify_safe_response(sensitive_view, check_for_vars=False)
     461
     462    def test_paranoid_request(self):
     463        """
     464        Ensure that no POST parameters can be seen in the default error reports
     465        for "paranoid" requests.
     466        """
     467        with self.settings(DEBUG=True):
     468            self.verify_unsafe_response(paranoid_view, check_for_vars=False)
     469
     470        with self.settings(DEBUG=False):
     471            self.verify_paranoid_response(paranoid_view, check_for_vars=False)
     472
     473    def test_custom_exception_reporter_filter(self):
     474        """
     475        Ensure that it's possible to assign an exception reporter filter to
     476        the request to bypass the one set in DEFAULT_EXCEPTION_REPORTER_FILTER.
     477        """
     478        with self.settings(DEBUG=True):
     479            self.verify_unsafe_response(custom_exception_reporter_filter_view,
     480                check_for_vars=False)
     481
     482        with self.settings(DEBUG=False):
     483            self.verify_unsafe_response(custom_exception_reporter_filter_view,
     484                check_for_vars=False)
Back to Top