Ticket #14614: 14614.sensitive-request.2.diff

File 14614.sensitive-request.2.diff, 14.0 KB (added by Julien Phalip, 13 years ago)

Moved the @sensitive decorator to django.views.decorators.security and marked auth admin views as sensitive

  • django/contrib/auth/admin.py

    diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
    index 7d855d8..aaca526 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
    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
    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
    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..298d6a2 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
    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 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..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..45836b5 100644
    a b class ExceptionReporter(object):  
    123123            'is_email': self.is_email,
    124124            'unicode_hint': unicode_hint,
    125125            'frames': frames,
    126             'request': self.request,
     126            'request': self.request if self.display_sensitive_info() else None,
    127127            'settings': get_safe_settings(),
    128128            'sys_executable': sys.executable,
    129129            'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
    class ExceptionReporter(object):  
    143143            c['lastframe'] = frames[-1]
    144144        return t.render(c)
    145145
     146    def display_sensitive_info(self):
     147        if (not settings.DEBUG and
     148            self.request is not None and
     149            hasattr(self.request, 'is_sensitive') and
     150            self.request.is_sensitive):
     151            return False
     152        else:
     153            return True
     154   
    146155    def get_template_exception_info(self):
    147156        origin, (start, end) = self.exc_value.source
    148157        template_source = origin.reload()
    class ExceptionReporter(object):  
    239248                    'filename': filename,
    240249                    'function': function,
    241250                    'lineno': lineno + 1,
    242                     'vars': tb.tb_frame.f_locals.items(),
     251                    'vars': tb.tb_frame.f_locals.items() if self.display_sensitive_info() else [],
    243252                    'id': id(tb),
    244253                    'pre_context': pre_context,
    245254                    'context_line': context_line,
  • 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..c12dc3c
    - +  
     1import functools
     2
     3def sensitive(view):
     4    @functools.wraps(view)
     5    def wrapper(request, *args, **kwargs):
     6        request.is_sensitive = True
     7        return view(request, *args, **kwargs)
     8    return wrapper
  • tests/regressiontests/views/tests/debug.py

    diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py
    index 6dd4bd4..c67cff3 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
    1317
    1418
    1519class DebugViewTests(TestCase):
    class DebugViewTests(TestCase):  
    6367
    6468class ExceptionReporterTests(TestCase):
    6569    rf = RequestFactory()
     70    breakfast_data = {'sausage-key': 'sausage-value',
     71                      'baked-beans-key': 'baked-beans-value',
     72                      'hash-brown-key': 'hash-brown-value',
     73                      'bacon-key': 'bacon-value',}
    6674
    6775    def test_request_and_exception(self):
    6876        "A simple exception report can be generated"
    class ExceptionReporterTests(TestCase):  
    8391        self.assertIn('<h2>Request information</h2>', html)
    8492        self.assertNotIn('<p>Request data not supplied</p>', html)
    8593
     94    def verify_unsafe_response(self, view):
     95        """
     96        Asserts that potentially sensitive info are displayed in the response.
     97        """
     98        request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     99        response = view(request)
     100        self.assertContains(response, 'cooked_eggs', status_code=500)
     101        self.assertContains(response, 'scrambled', status_code=500)
     102        for k, v in self.breakfast_data.items():
     103            self.assertContains(response, k, status_code=500)
     104            self.assertContains(response, v, status_code=500)
     105   
     106    def verify_unsafe_email(self, view):
     107        """
     108        Asserts that potentially sensitive info are displayed in the error email.
     109        """
     110        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     111            mail.outbox = [] # Empty outbox
     112            request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     113            response = view(request)
     114           
     115            # Frames vars are never shown in plain text error emails
     116            self.assertEquals(len(mail.outbox), 1)
     117            email = mail.outbox[0]
     118            self.assertIn('cooked_eggs', email.body)
     119            self.assertIn('scrambled', email.body)
     120            for k, v in self.breakfast_data.items():
     121                self.assertIn(k, email.body)
     122                self.assertIn(v, email.body)
     123       
     124    def verify_safe_response(self, view):
     125        """
     126        Asserts that potentially sensitive info are not displayed in the response.
     127        """
     128        request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     129        response = view(request)
     130        self.assertContains(response, 'cooked_eggs', status_code=500)
     131        self.assertNotContains(response, 'scrambled', status_code=500)
     132        for k, v in self.breakfast_data.items():
     133            self.assertNotContains(response, k, status_code=500)
     134            self.assertNotContains(response, v, status_code=500)
     135   
     136    def verify_safe_email(self, view):
     137        """
     138        Asserts that potentially sensitive info are not displayed in the error email.
     139        """
     140        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
     141            mail.outbox = [] # Empty outbox
     142            request = self.rf.get('/some_url/', dict(cooked_eggs='scrambled', **self.breakfast_data))
     143            response = view(request)
     144           
     145            # Frames vars are never shown in plain text error emails
     146            self.assertEquals(len(mail.outbox), 1)
     147            email = mail.outbox[0]
     148            self.assertIn('cooked_eggs', email.body)
     149            self.assertNotIn('scrambled', email.body)
     150            for k, v in self.breakfast_data.items():
     151                self.assertNotIn(k, email.body)
     152                self.assertNotIn(v, email.body)
     153               
     154    def test_non_sensitive_request(self):
     155        """
     156        Ensure that everything (request info and frame vars) can bee seen
     157        in the default exception reporters for views that are not marked as
     158        sensitive.
     159        Refs #14614.
     160        """
     161        with self.settings(DEBUG=True):
     162            self.verify_unsafe_response(non_sensitive_view)
     163            self.verify_unsafe_email(non_sensitive_view)
     164               
     165        with self.settings(DEBUG=False):
     166            self.verify_unsafe_response(non_sensitive_view)
     167            self.verify_unsafe_email(non_sensitive_view)
     168       
     169    def test_sensitive_request(self):
     170        """
     171        Ensure that the request info and frame vars cannot be seen
     172        in the default exception reporters for views that are marked as
     173        sensitive.
     174        Refs #14614.
     175        """
     176        with self.settings(DEBUG=True):
     177            self.verify_unsafe_response(sensitive_view)
     178            self.verify_unsafe_email(sensitive_view)
     179           
     180        with self.settings(DEBUG=False):
     181            self.verify_safe_response(sensitive_view)
     182            self.verify_safe_email(sensitive_view)
     183           
    86184    def test_no_request(self):
    87185        "An exception report can be generated without request"
    88186        try:
  • tests/regressiontests/views/views.py

    diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py
    index 11d289f..acc7ef2 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
     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