Ticket #14614: 14614.sensitive-request.2.diff
File 14614.sensitive-request.2.diff, 14.0 KB (added by , 13 years ago) |
---|
-
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 12 12 from django.utils.decorators import method_decorator 13 13 from django.utils.translation import ugettext, ugettext_lazy as _ 14 14 from django.views.decorators.csrf import csrf_protect 15 from django.views.decorators.security import sensitive 15 16 16 17 csrf_protect_m = method_decorator(csrf_protect) 17 18 … … class UserAdmin(admin.ModelAdmin): 78 79 (r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password)) 79 80 ) + super(UserAdmin, self).get_urls() 80 81 82 @sensitive 81 83 @csrf_protect_m 82 84 @transaction.commit_on_success 83 85 def add_view(self, request, form_url='', extra_context=None): … … class UserAdmin(admin.ModelAdmin): 102 104 extra_context.update(defaults) 103 105 return super(UserAdmin, self).add_view(request, form_url, extra_context) 104 106 107 @sensitive 105 108 def user_change_password(self, request, id): 106 109 if not self.has_change_permission(request): 107 110 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 6 6 from django.template.response import TemplateResponse 7 7 from django.utils.http import base36_to_int 8 8 from django.utils.translation import ugettext as _ 9 from django.views.decorators.security import sensitive 9 10 from django.views.decorators.cache import never_cache 10 11 from django.views.decorators.csrf import csrf_protect 11 12 … … from django.contrib.auth.models import User 17 18 from django.contrib.auth.tokens import default_token_generator 18 19 from django.contrib.sites.models import get_current_site 19 20 20 21 @sensitive 21 22 @csrf_protect 22 23 @never_cache 23 24 def login(request, template_name='registration/login.html', … … def password_reset_done(request, 175 176 current_app=current_app) 176 177 177 178 # Doesn't need csrf_protect since no-one can guess the URL 179 @sensitive 178 180 @never_cache 179 181 def password_reset_confirm(request, uidb36=None, token=None, 180 182 template_name='registration/password_reset_confirm.html', … … def password_reset_complete(request, 227 229 return TemplateResponse(request, template_name, context, 228 230 current_app=current_app) 229 231 232 @sensitive 230 233 @csrf_protect 231 234 @login_required 232 235 def 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): 206 206 exc_info=exc_info, 207 207 extra={ 208 208 'status_code': 500, 209 'request': request209 'request': request 210 210 } 211 211 ) 212 212 -
django/utils/log.py
diff --git a/django/utils/log.py b/django/utils/log.py index 93e38d1..3899b8c 100644
a b 1 1 import logging 2 2 import sys 3 3 4 from django.core import mail 5 from django.conf import settings 4 6 5 7 # Make sure a NullHandler is available 6 8 # This was added in Python 2.7/3.2 … … class AdminEmailHandler(logging.Handler): 42 44 from django.conf import settings 43 45 from django.views.debug import ExceptionReporter 44 46 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): 54 51 subject = '%s: %s' % ( 55 record.levelname, 56 record.msg 57 ) 58 52 record.levelname, 53 record.msg 54 ) 59 55 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 62 74 if record.exc_info: 63 75 exc_info = record.exc_info 64 76 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): 123 123 'is_email': self.is_email, 124 124 'unicode_hint': unicode_hint, 125 125 'frames': frames, 126 'request': self.request ,126 'request': self.request if self.display_sensitive_info() else None, 127 127 'settings': get_safe_settings(), 128 128 'sys_executable': sys.executable, 129 129 'sys_version_info': '%d.%d.%d' % sys.version_info[0:3], … … class ExceptionReporter(object): 143 143 c['lastframe'] = frames[-1] 144 144 return t.render(c) 145 145 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 146 155 def get_template_exception_info(self): 147 156 origin, (start, end) = self.exc_value.source 148 157 template_source = origin.reload() … … class ExceptionReporter(object): 239 248 'filename': filename, 240 249 'function': function, 241 250 '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 [], 243 252 'id': id(tb), 244 253 'pre_context': pre_context, 245 254 '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
- + 1 import functools 2 3 def 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 1 from __future__ import with_statement 1 2 import inspect 2 3 import os 3 4 import sys … … from django.test import TestCase, RequestFactory 8 9 from django.core.urlresolvers import reverse 9 10 from django.template import TemplateSyntaxError 10 11 from django.views.debug import ExceptionReporter 12 from django.core.exceptions import ImproperlyConfigured 13 from django.core import mail 11 14 12 15 from regressiontests.views import BrokenException, except_args 16 from regressiontests.views.views import sensitive_view, non_sensitive_view 13 17 14 18 15 19 class DebugViewTests(TestCase): … … class DebugViewTests(TestCase): 63 67 64 68 class ExceptionReporterTests(TestCase): 65 69 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',} 66 74 67 75 def test_request_and_exception(self): 68 76 "A simple exception report can be generated" … … class ExceptionReporterTests(TestCase): 83 91 self.assertIn('<h2>Request information</h2>', html) 84 92 self.assertNotIn('<p>Request data not supplied</p>', html) 85 93 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 86 184 def test_no_request(self): 87 185 "An exception report can be generated without request" 88 186 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 6 6 from django.shortcuts import render_to_response, render 7 7 from django.template import Context, RequestContext, TemplateDoesNotExist 8 8 from django.views.debug import technical_500_response 9 from django.views.decorators.security import sensitive 10 from django.utils.log import getLogger 9 11 10 12 from regressiontests.views import BrokenException, except_args 11 13 … … def raises_template_does_not_exist(request): 128 130 return render_to_response('i_dont_exist.html') 129 131 except TemplateDoesNotExist: 130 132 return technical_500_response(request, *sys.exc_info()) 133 134 def 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 150 def sensitive_view(request): 151 return non_sensitive_view(request)