Ticket #14614: 14614.sensitive-request.diff

File 14614.sensitive-request.diff, 13.2 KB (added by Julien Phalip, 14 years ago)
  • django/contrib/auth/views.py

    diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
    index cfb2659..72ca81c 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.debug import sensitive
    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
    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
    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
    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 f216886..6ccf5b2 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..3899b8c 100644
    a b  
    11import logging
    22import sys
     3
    34from django.core import mail
     5from django.conf import settings
    46
    57# Make sure a NullHandler is available
    68# This was added in Python 2.7/3.2
    class AdminEmailHandler(logging.Handler):  
    4244        from django.conf import settings
    4345        from django.views.debug import ExceptionReporter
    4446
    45         try:
    46             request = record.request
    47             subject = '%s (%s IP): %s' % (
    48                 record.levelname,
    49                 (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'),
    50                 record.msg
    51             )
    52             request_repr = repr(request)
    53         except:
     47        if (not settings.DEBUG and
     48            hasattr(record, 'request') and
     49            hasattr(record.request, 'is_sensitive') and
     50            record.request.is_sensitive):
    5451            subject = '%s: %s' % (
    55                 record.levelname,
    56                 record.msg
    57             )
    58 
     52                    record.levelname,
     53                    record.msg
     54                )
    5955            request = None
    60             request_repr = "Request repr() unavailable"
    61 
     56            request_repr = "Request is sensitive, so it is not provided in this report."
     57        else:
     58            try:
     59                request = record.request
     60                subject = '%s (%s IP): %s' % (
     61                    record.levelname,
     62                    (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'),
     63                    record.msg
     64                )
     65                request_repr = repr(request)
     66            except:
     67                subject = '%s: %s' % (
     68                    record.levelname,
     69                    record.msg
     70                )
     71                request = None
     72                request_repr = "Request repr() unavailable."
     73               
    6274        if record.exc_info:
    6375            exc_info = record.exc_info
    6476            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..11bcb07 100644
    a b import os  
    33import re
    44import sys
    55import types
     6import functools
    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
     19def sensitive(view):
     20    @functools.wraps(view)
     21    def wrapper(request, *args, **kwargs):
     22        request.is_sensitive = True
     23        return view(request, *args, **kwargs)
     24    return wrapper
     25
    1826def linebreak_iter(template_source):
    1927    yield 0
    2028    p = template_source.find('\n')
    class ExceptionReporter(object):  
    123131            'is_email': self.is_email,
    124132            'unicode_hint': unicode_hint,
    125133            'frames': frames,
    126             'request': self.request,
     134            'request': self.request if self.display_sensitive_info() else None,
    127135            'settings': get_safe_settings(),
    128136            'sys_executable': sys.executable,
    129137            'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
    class ExceptionReporter(object):  
    143151            c['lastframe'] = frames[-1]
    144152        return t.render(c)
    145153
     154    def display_sensitive_info(self):
     155        if (not settings.DEBUG and
     156            self.request is not None and
     157            hasattr(self.request, 'is_sensitive') and
     158            self.request.is_sensitive):
     159            return False
     160        else:
     161            return True
     162   
    146163    def get_template_exception_info(self):
    147164        origin, (start, end) = self.exc_value.source
    148165        template_source = origin.reload()
    class ExceptionReporter(object):  
    239256                    'filename': filename,
    240257                    'function': function,
    241258                    'lineno': lineno + 1,
    242                     'vars': tb.tb_frame.f_locals.items(),
     259                    'vars': tb.tb_frame.f_locals.items() if self.display_sensitive_info() else [],
    243260                    'id': id(tb),
    244261                    'pre_context': pre_context,
    245262                    'context_line': context_line,
  • tests/regressiontests/views/tests/debug.py

    diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py
    index d778311..3ea5d90 100644
    a b  
     1from __future__ import with_statement
    12import inspect
    23import sys
    34
    45from django.conf import settings
     6from django.core.exceptions import ImproperlyConfigured
    57from django.core.files.uploadedfile import SimpleUploadedFile
    68from django.test import TestCase, RequestFactory
    79from django.core.urlresolvers import reverse
    810from django.template import TemplateSyntaxError
    911from django.views.debug import ExceptionReporter
     12from django.core import mail
    1013
    1114from regressiontests.views import BrokenException, except_args
     15from regressiontests.views.views import sensitive_view, non_sensitive_view
    1216
    1317
    1418class DebugViewTests(TestCase):
    class DebugViewTests(TestCase):  
    6165
    6266class ExceptionReporterTests(TestCase):
    6367    rf = RequestFactory()
     68    breakfast_data = {'sausage-key': 'sausage-value',
     69                      'baked-beans-key': 'baked-beans-value',
     70                      'hash-brown-key': 'hash-brown-value',
     71                      'bacon-key': 'bacon-value',}
    6472
    6573    def test_request_and_exception(self):
    6674        "A simple exception report can be generated"
    class ExceptionReporterTests(TestCase):  
    8189        self.assertIn('<h2>Request information</h2>', html)
    8290        self.assertNotIn('<p>Request data not supplied</p>', html)
    8391
     92    def verify_unsafe_response(self, view):
     93        """
     94        Asserts that potentially sensitive info are displayed in the response.
     95        """
     96        request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     97        response = view(request)
     98        self.assertContains(response, 'cooked_eggs', status_code=500)
     99        self.assertContains(response, 'scrambled', status_code=500)
     100        for k, v in self.breakfast_data.items():
     101            self.assertContains(response, k, status_code=500)
     102            self.assertContains(response, v, status_code=500)
     103   
     104    def verify_unsafe_email(self, view):
     105        """
     106        Asserts that potentially sensitive info are displayed in the error email.
     107        """
     108        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     109            mail.outbox = [] # Empty outbox
     110            request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     111            response = view(request)
     112           
     113            # TODO: Figure out why 2 emails get sent: len(outbox)==2
     114            # Frames vars are never shown in plain text error emails
     115            email = mail.outbox[0]
     116            self.assertIn('cooked_eggs', email.body)
     117            self.assertIn('scrambled', email.body)
     118            for k, v in self.breakfast_data.items():
     119                self.assertIn(k, email.body)
     120                self.assertIn(v, email.body)
     121       
     122    def verify_safe_response(self, view):
     123        """
     124        Asserts that potentially sensitive info are not displayed in the response.
     125        """
     126        request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     127        response = view(request)
     128        self.assertContains(response, 'cooked_eggs', status_code=500)
     129        self.assertNotContains(response, 'scrambled', status_code=500)
     130        for k, v in self.breakfast_data.items():
     131            self.assertNotContains(response, k, status_code=500)
     132            self.assertNotContains(response, v, status_code=500)
     133   
     134    def verify_safe_email(self, view):
     135        """
     136        Asserts that potentially sensitive info are not displayed in the error email.
     137        """
     138        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     139            mail.outbox = [] # Empty outbox
     140            request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     141            response = view(request)
     142           
     143            # TODO: Figure out why 2 emails get sent: len(outbox)==2
     144            # Frames vars are never shown in plain text error emails
     145            email = mail.outbox[0]
     146            self.assertIn('cooked_eggs', email.body)
     147            self.assertNotIn('scrambled', email.body)
     148            for k, v in self.breakfast_data.items():
     149                self.assertNotIn(k, email.body)
     150                self.assertNotIn(v, email.body)
     151               
     152    def test_non_sensitive_request(self):
     153        """
     154        Ensure that everything (request info and frame vars) can bee seen
     155        in the default exception reporters for views that are not marked as
     156        sensitive.
     157        Refs #14614.
     158        """
     159        with self.settings(DEBUG=True):
     160            self.verify_unsafe_response(non_sensitive_view)
     161            self.verify_unsafe_email(non_sensitive_view)
     162               
     163        with self.settings(DEBUG=False):
     164            self.verify_unsafe_response(non_sensitive_view)
     165            self.verify_unsafe_email(non_sensitive_view)
     166       
     167    def test_sensitive_request(self):
     168        """
     169        Ensure that the request info and frame vars cannot be seen
     170        in the default exception reporters for views that are marked as
     171        sensitive.
     172        Refs #14614.
     173        """
     174        with self.settings(DEBUG=True):
     175            self.verify_unsafe_response(sensitive_view)
     176            self.verify_unsafe_email(sensitive_view)
     177           
     178        with self.settings(DEBUG=False):
     179            self.verify_safe_response(sensitive_view)
     180            self.verify_safe_email(sensitive_view)
     181           
    84182    def test_no_request(self):
    85183        "An exception report can be generated without request"
    86184        try:
  • tests/regressiontests/views/views.py

    diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py
    index 11d289f..e36f919 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.debug import sensitive
     10from django.utils.log import getLogger
    911
    1012from regressiontests.views import BrokenException, except_args
    1113
    def raises_template_does_not_exist(request):  
    128130        return render_to_response('i_dont_exist.html')
    129131    except TemplateDoesNotExist:
    130132        return technical_500_response(request, *sys.exc_info())
     133
     134def non_sensitive_view(request):
     135    try:
     136        cooked_eggs = request.get('cooked_eggs')
     137        raise Exception
     138    except Exception:
     139        logger = getLogger('django.request')
     140        logger.error('Internal Server Error: %s' % request.path,
     141            exc_info=sys.exc_info(),
     142            extra={
     143                'status_code': 500,
     144                'request': request
     145            }
     146        )
     147        return technical_500_response(request, *sys.exc_info())
     148   
     149@sensitive
     150def sensitive_view(request):
     151    return non_sensitive_view(request)
Back to Top