Ticket #14614: 14614.exception-reporter-filter.2.diff
File 14614.exception-reporter-filter.2.diff, 40.9 KB (added by , 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 = { 537 537 } 538 538 } 539 539 540 # Default exception reporter filter class used in case none has been 541 # specifically assigned to the HttpRequest instance. 542 DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFilter' 543 540 544 ########### 541 545 # TESTING # 542 546 ########### -
django/contrib/auth/admin.py
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index 7d855d8..237b153 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.debug import sensitive_post_parameters 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_post_parameters() 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_post_parameters() 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..6a97ffa 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.debug import sensitive_post_parameters 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_post_parameters() 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_post_parameters() 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_post_parameters() 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..c1cd328 100644
a b 1 1 import logging 2 2 import sys 3 import traceback 4 5 from django.conf import settings 3 6 from django.core import mail 7 from django.views.debug import ExceptionReporter, get_exception_reporter_filter 4 8 5 9 # Make sure a NullHandler is available 6 10 # This was added in Python 2.7/3.2 … … class AdminEmailHandler(logging.Handler): 35 39 """An exception log handler that emails log entries to site admins. 36 40 37 41 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. 39 43 """ 40 44 def emit(self, record): 41 import traceback42 from django.conf import settings43 from django.views.debug import ExceptionReporter44 45 45 try: 46 46 request = record.request 47 47 subject = '%s (%s IP): %s' % ( … … class AdminEmailHandler(logging.Handler): 49 49 (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'), 50 50 record.msg 51 51 ) 52 request_repr = repr(request) 52 filter = get_exception_reporter_filter(request) 53 request_repr = filter.get_request_repr(request) 53 54 except: 54 55 subject = '%s: %s' % ( 55 56 record.levelname, 56 57 record.msg 57 58 ) 58 59 59 request = None 60 request_repr = "Request repr() unavailable "61 60 request_repr = "Request repr() unavailable." 61 62 62 if record.exc_info: 63 63 exc_info = record.exc_info 64 64 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..9ee1cd9 100644
a b import os 3 3 import re 4 4 import sys 5 5 import types 6 from pprint import pformat 6 7 7 8 from django.conf import settings 8 from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound 9 from django.http import (HttpResponse, HttpResponseServerError, 10 HttpResponseNotFound, HttpRequest) 9 11 from django.template import (Template, Context, TemplateDoesNotExist, 10 12 TemplateSyntaxError) 11 13 from django.template.defaultfilters import force_escape, pprint … … from django.utils.encoding import smart_unicode, smart_str 15 17 16 18 HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD|PROFANITIES_LIST|SIGNATURE') 17 19 20 CLEANSED_SUBSTITUTE = u'********************' 21 18 22 def linebreak_iter(template_source): 19 23 yield 0 20 24 p = template_source.find('\n') … … def cleanse_setting(key, value): 31 35 """ 32 36 try: 33 37 if HIDDEN_SETTINGS.search(key): 34 cleansed = '********************'38 cleansed = CLEANSED_SUBSTITUTE 35 39 else: 36 40 if isinstance(value, dict): 37 41 cleansed = dict((k, cleanse_setting(k, v)) for k,v in value.items()) … … def technical_500_response(request, exc_type, exc_value, tb): 59 63 html = reporter.get_traceback_html() 60 64 return HttpResponseServerError(html, mimetype='text/html') 61 65 66 # Cache for the default exception reporter filter class. 67 default_exception_reporter_filter = None 68 69 def get_exception_reporter_filter(request): 70 global default_exception_reporter_filter 71 if default_exception_reporter_filter is None: 72 # Load the default filter for the first time and cache it. 73 modpath = settings.DEFAULT_EXCEPTION_REPORTER_FILTER 74 modname, classname = modpath.rsplit('.', 1) 75 try: 76 mod = import_module(modname) 77 except ImportError, e: 78 raise ImproperlyConfigured( 79 'Error importing default exception reporter filter %s: "%s"' % (modpath, e)) 80 try: 81 default_exception_reporter_filter = getattr(mod, classname) 82 except AttributeError: 83 raise exceptions.ImproperlyConfigured('Default exception reporter filter module "%s" does not define a "%s" class' % (modname, classname)) 84 if request: 85 filter = getattr(request, 'exception_reporter_filter', default_exception_reporter_filter) 86 return filter() 87 else: 88 return default_exception_reporter_filter() 89 90 class ExceptionReporterFilter(object): 91 """ 92 Base for all exception reporter filter classes. All overridable hooks 93 contain lenient default behaviours. 94 """ 95 96 def get_request_repr(self, request): 97 if request is None: 98 return repr(None) 99 else: 100 # Mimic the request's (WSGIRequest, ModPythonRequest or 101 # HttpRequest) own __repr__() method and and filter out the POST 102 # parameters if necessary. 103 _repr = re.sub(r'(.*)\nGET:(.*),\nPOST:(.*),\nCOOKIES:([.\n]*)', 104 r'\1\nGET:\2,\nPOST:%s,\nCOOKIES:\4' % self.get_post_parameters(request), 105 repr(request) 106 ) 107 return _repr 108 109 def get_post_parameters(self, request): 110 if request is None: 111 return {} 112 else: 113 return request.POST 114 115 def get_traceback_frame_variables(self, request, tb_frame): 116 return tb_frame.f_locals.items() 117 118 class SafeExceptionReporterFilter(ExceptionReporterFilter): 119 """ 120 Use annotations made by the sensitive_post_parameters and 121 sensitive_variables decorators to filter out sensitive information. 122 """ 123 124 def _is_active(self, request): 125 """ 126 This filter is to add safety in production environments (i.e. DEBUG 127 is False). If DEBUG is True then your site is not safe anyway. 128 This hook is provided as a convenience to easily activate or 129 deactivate the filter on a per request basis. 130 """ 131 return settings.DEBUG is False 132 133 def get_post_parameters(self, request): 134 """ 135 Replaces the values of POST parameters marked as sensitive with 136 stars (*********). 137 """ 138 if request is None: 139 return {} 140 else: 141 sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', []) 142 if self._is_active(request) and sensitive_post_parameters: 143 cleansed = request.POST.copy() 144 if sensitive_post_parameters == '__ALL__': 145 # Cleanse all parameters. 146 for k, v in cleansed.items(): 147 cleansed[k] = CLEANSED_SUBSTITUTE 148 return cleansed 149 else: 150 # Cleanse only the specified parameters. 151 for param in sensitive_post_parameters: 152 if cleansed.has_key(param): 153 cleansed[param] = CLEANSED_SUBSTITUTE 154 return cleansed 155 else: 156 return request.POST 157 158 def get_traceback_frame_variables(self, request, tb_frame): 159 """ 160 Replaces the values of variables marked as sensitive with 161 stars (*********). 162 """ 163 func_name = tb_frame.f_code.co_name 164 func = tb_frame.f_globals.get(func_name) 165 sensitive_variables = getattr(func, 'sensitive_variables', []) 166 cleansed = [] 167 if self._is_active(request) and sensitive_variables: 168 if sensitive_variables == '__ALL__': 169 # Cleanse all variables 170 for name, value in tb_frame.f_locals.items(): 171 cleansed.append((name, CLEANSED_SUBSTITUTE)) 172 return cleansed 173 else: 174 # Cleanse specified variables 175 for name, value in tb_frame.f_locals.items(): 176 if name in sensitive_variables: 177 value = CLEANSED_SUBSTITUTE 178 elif isinstance(value, HttpRequest): 179 # Cleanse the request's POST parameters. 180 value = self.get_request_repr(value) 181 cleansed.append((name, value)) 182 return cleansed 183 else: 184 # Potentially cleanse only the request if it's one of the frame variables. 185 for name, value in tb_frame.f_locals.items(): 186 if isinstance(value, HttpRequest): 187 # Cleanse the request's POST parameters. 188 value = self.get_request_repr(value) 189 cleansed.append((name, value)) 190 return cleansed 191 62 192 class ExceptionReporter(object): 63 193 """ 64 194 A class to organize and coordinate reporting on exceptions. 65 195 """ 66 196 def __init__(self, request, exc_type, exc_value, tb, is_email=False): 67 197 self.request = request 198 self.filter = get_exception_reporter_filter(self.request) 68 199 self.exc_type = exc_type 69 200 self.exc_value = exc_value 70 201 self.tb = tb … … class ExceptionReporter(object): 124 255 'unicode_hint': unicode_hint, 125 256 'frames': frames, 126 257 'request': self.request, 258 'filtered_POST': self.filter.get_post_parameters(self.request), 127 259 'settings': get_safe_settings(), 128 260 'sys_executable': sys.executable, 129 261 'sys_version_info': '%d.%d.%d' % sys.version_info[0:3], … … class ExceptionReporter(object): 222 354 frames = [] 223 355 tb = self.tb 224 356 while tb is not None: 225 # support for __traceback_hide__ which is used by a few libraries357 # Support for __traceback_hide__ which is used by a few libraries 226 358 # to hide internal frames. 227 359 if tb.tb_frame.f_locals.get('__traceback_hide__'): 228 360 tb = tb.tb_next … … class ExceptionReporter(object): 239 371 'filename': filename, 240 372 'function': function, 241 373 'lineno': lineno + 1, 242 'vars': tb.tb_frame.f_locals.items(),374 'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame), 243 375 'id': id(tb), 244 376 'pre_context': pre_context, 245 377 'context_line': context_line, … … class ExceptionReporter(object): 247 379 'pre_context_lineno': pre_context_lineno + 1, 248 380 }) 249 381 tb = tb.tb_next 250 251 382 return frames 252 383 253 384 def format_exception(self): … … Exception Value: {{ exception_value|force_escape }} 643 774 {% endif %} 644 775 645 776 <h3 id="post-info">POST</h3> 646 {% if request.POST %}777 {% if filtered_POST %} 647 778 <table class="req"> 648 779 <thead> 649 780 <tr> … … Exception Value: {{ exception_value|force_escape }} 652 783 </tr> 653 784 </thead> 654 785 <tbody> 655 {% for var in request.POST.items %}786 {% for var in filtered_POST.items %} 656 787 <tr> 657 788 <td>{{ var.0 }}</td> 658 789 <td class="code"><pre>{{ var.1|pprint }}</pre></td> -
new file django/views/decorators/debug.py
diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py new file mode 100644 index 0000000..9974629
- + 1 import functools 2 3 4 def 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('user', 'password', 'credit_card') 15 def my_function(user): 16 password = user.pass_word 17 credit_card = user.credit_card_number 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 = '__ALL__' 34 return func(*args, **kwargs) 35 return wrapper 36 return decorator 37 38 39 def sensitive_post_parameters(*parameters): 40 """ 41 Indicates which POST parameters used in the decorated view are sensitive, 42 so that those parameters can later be treated in a special way, for example 43 by hiding them when logging unhandled exceptions. 44 45 Two forms are accepted: 46 47 * with specified parameters: 48 49 @sensitive_post_parameters('password', 'credit_card') 50 def my_view(request): 51 pw = request.POST['password'] 52 cc = request.POST['credit_card'] 53 ... 54 55 * without any specified parameters, in which case it is assumed that 56 all parameters are considered sensitive: 57 58 @sensitive_post_parameters() 59 def my_view(request) 60 ... 61 """ 62 def decorator(view): 63 @functools.wraps(view) 64 def wrapper(request, *args, **kwargs): 65 if parameters: 66 request.sensitive_post_parameters = parameters 67 else: 68 request.sensitive_post_parameters = '__ALL__' 69 return view(request, *args, **kwargs) 70 return wrapper 71 return decorator -
docs/howto/error-reporting.txt
diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index c15c1d8..04a2343 100644
a b 1 Error reporting via email2 =============== ==========1 Error reporting 2 =============== 3 3 4 4 When you're running a public site you should always turn off the 5 5 :setting:`DEBUG` setting. That will make your server run much faster, and will … … revealed by the error pages. 9 9 However, running with :setting:`DEBUG` set to ``False`` means you'll never see 10 10 errors generated by your site -- everyone will just see your public error pages. 11 11 You need to keep track of errors that occur in deployed sites, so Django can be 12 configured to email you details ofthose errors.12 configured to create reports with details about those errors. 13 13 14 Server errors14 Email reports 15 15 ------------- 16 16 17 Server errors 18 ~~~~~~~~~~~~~ 19 17 20 When :setting:`DEBUG` is ``False``, Django will email the users listed in the 18 21 :setting:`ADMINS` setting whenever your code raises an unhandled exception and 19 22 results in an internal server error (HTTP status code 500). This gives the … … setting. 48 51 </topics/logging>`. 49 52 50 53 404 errors 51 ---------- 54 ~~~~~~~~~~ 52 55 53 56 Django can also be configured to email errors about broken links (404 "page 54 57 not found" errors). Django sends emails about 404 errors when: … … The best way to disable this behavior is to set 96 99 97 100 .. seealso:: 98 101 99 You can also set up custom error reporting by writing a custom piece of100 :ref:`exception middleware <exception-middleware>`. If you do write custom101 error handling, it's a good idea to emulate Django's built-in error handling102 and only report/log errors if :setting:`DEBUG` is ``False``.103 104 .. seealso::105 106 102 .. versionadded:: 1.3 107 103 108 104 404 errors are logged using the logging framework. By default, these log … … The best way to disable this behavior is to set 116 112 Previously, two settings were used to control which URLs not to report: 117 113 :setting:`IGNORABLE_404_STARTS` and :setting:`IGNORABLE_404_ENDS`. They 118 114 were replaced by :setting:`IGNORABLE_404_URLS`. 115 116 .. _filtering-error-reports: 117 118 Filtering error reports 119 ----------------------- 120 121 .. versionadded:: 1.4 122 123 Filtering sensitive information 124 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 126 Error reports are really helpful for debugging errors, so it is generally 127 useful to record as much relevant information about those errors as possible. 128 For example, by default Django records the `full traceback`_ for the 129 exception raised, each `traceback frame`_'s local variables, and the 130 :class:`HttpRequest`'s :ref:`attributes<httprequest-attributes>`. 131 132 However, sometimes certain types of information may be too sensitive and thus 133 may not be appropriate to be kept track of, for example a user's password or 134 credit card number. So Django offers a set of function decorators to help you 135 control which information should be filtered out of error reports in a 136 production environment (that is, where :setting:`DEBUG` is set to ``False``): 137 :func:`sensitive_variables` and :func:`sensitive_post_parameters`. 138 139 .. _`full traceback`: http://en.wikipedia.org/wiki/Stack_trace 140 .. _`traceback frame`: http://en.wikipedia.org/wiki/Stack_frame 141 142 .. function:: sensitive_variables(*variables) 143 144 If a function (either a view or any regular callback) in your code uses 145 local variables susceptible to contain sensitive information, you may 146 prevent the values of those variables from being included in error reports 147 using the ``sensitive_variables`` decorator: 148 149 .. code-block:: python 150 151 from django.views.decorators.debug import sensitive_variables 152 153 @sensitive_variables('user', 'pw', 'cc') 154 def process_info(user): 155 pw = user.pass_word 156 cc = user.credit_card_number 157 name = user.name 158 ... 159 160 In the above example, the values for the ``user``, ``pw`` and ``cc`` 161 variables will be hidden and replaced with stars (`**********`) in the 162 error reports, whereas the value of the ``name`` variable will be 163 disclosed. 164 165 To systematically hide all local variables of a function from error logs, 166 do not provide any argument to the ``sensitive_variables`` decorator: 167 168 .. code-block:: python 169 170 @sensitive_variables() 171 def my_function(): 172 ... 173 174 .. function:: sensitive_post_parameters(*parameters) 175 176 If one of your views receives an :class:`HttpRequest` object with 177 :attr:`POST parameters<HttpRequest.POST>` susceptible to contain sensitive 178 information, you may prevent the values of those parameters from being 179 included in the error reports using the ``sensitive_post_parameters`` 180 decorator: 181 182 .. code-block:: python 183 184 from django.views.decorators.debug import sensitive_post_parameters 185 186 @sensitive_post_parameters('pass_word', 'credit_card_number') 187 def record_user_profile(request): 188 UserProfile.create(user=request.user, 189 password=request.POST['pass_word'], 190 credit_card=request.POST['credit_card_number'], 191 name=request.POST['name']) 192 ... 193 194 In the above example, the values for the ``pass_word`` and 195 ``credit_card_number`` POST parameters will be hidden and replaced with 196 stars (`**********`) in the request's representation inside the error 197 reports, whereas the value of the ``name`` parameter will be disclosed. 198 199 To systematically hide all POST parameters of a request in error reports, 200 do not provide any argument to the ``sensitive_post_parameters`` decorator: 201 202 .. code-block:: python 203 204 @sensitive_post_parameters() 205 def my_view(request): 206 ... 207 208 .. note:: 209 210 .. versionchanged:: 1.4 211 212 Since version 1.4, all POST parameters are systematically filtered out of 213 error reports for certain :mod:`contrib.views.auth` views (``login``, 214 ``password_reset_confirm``, ``password_change``, and ``add_view`` and 215 ``user_change_password`` in the ``auth`` admin) to prevent the leaking of 216 sensitive information such as user passwords. 217 218 .. _custom-error-reports: 219 220 Custom error reports 221 ~~~~~~~~~~~~~~~~~~~~ 222 223 All :func:`sensitive_variables` and :func:`sensitive_post_parameters` do is, 224 respectively, annotate the decorated function with the names of sensitive 225 variables and annotate the ``HttpRequest`` object with the names of sensitive 226 POST parameters, so that this sensitive information can later be filtered out 227 of reports when an error occurs. The actual filtering is done by Django's 228 default error reporter filter: 229 :class:`django.views.debug.SafeExceptionReporterFilter`. This filter uses the 230 decorators' annotations to replace the corresponding values with stars 231 (`**********`) when the error reports are produced. If you wish to override or 232 customize this default behavior, you need to define your own filter class and 233 tell Django to use it via the :setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` 234 setting: 235 236 .. code-block:: python 237 238 DEFAULT_EXCEPTION_REPORTER_FILTER = 'path.to.your.CustomExceptionReporterFilter' 239 240 Your custom filter class needs to inherit from 241 :class:`django.views.debug.ExceptionReporterFilter` and may override the 242 following methods. 243 244 .. class:: django.views.debug.ExceptionReporterFilter 245 246 .. method:: ExceptionReporterFilter.get_request_repr(self, request) 247 248 Returns the representation string of the request object, that is, the 249 value that would be returned by ``repr(request)``, except it filters 250 out the POST parameters as determined by 251 :meth:`ExceptionReporterFilter.get_post_parameters`. 252 253 .. method:: ExceptionReporterFilter.get_post_parameters(self, request) 254 255 Returns the filtered dictionary of POST parameters. For example, 256 :class:`django.views.debug.SafeExceptionReporterFilter` replaces the 257 values of sensitive parameters with stars (`**********`). 258 259 .. method:: ExceptionReporterFilter.get_traceback_frame_variables(self, request, tb_frame) 260 261 Returns the filtered dictionary of local variables for the given traceback 262 frame. For example, 263 :class:`django.views.debug.SafeExceptionReporterFilter` replaces the 264 values of sensitive variables with stars (`**********`). 265 266 .. seealso:: 267 268 You can also set up custom error reporting by writing a custom piece of 269 :ref:`exception middleware <exception-middleware>`. If you do write custom 270 error handling, it's a good idea to emulate Django's built-in error handling 271 and only report/log errors if :setting:`DEBUG` is ``False``. -
docs/ref/request-response.txt
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 72872d5..ff04e25 100644
a b HttpRequest objects 23 23 24 24 .. class:: HttpRequest 25 25 26 .. _httprequest-attributes: 27 26 28 Attributes 27 29 ---------- 28 30 -
docs/ref/settings.txt
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 816c3e9..e95c165 100644
a b Default content type to use for all ``HttpResponse`` objects, if a MIME type 772 772 isn't manually specified. Used with :setting:`DEFAULT_CHARSET` to construct 773 773 the ``Content-Type`` header. 774 774 775 .. setting:: DEFAULT_EXCEPTION_REPORTER_FILTER 776 777 DEFAULT_EXCEPTION_REPORTER_FILTER 778 --------------------------------- 779 780 Default: :class:`django.views.debug.SafeExceptionReporterFilter` 781 782 Default exception reporter filter class to be used if none has been assigned to 783 the :class:`HttpRequest` instance yet. 784 See :ref:`Filtering error reports<filtering-error-reports>`. 785 775 786 .. setting:: DEFAULT_FILE_STORAGE 776 787 777 788 DEFAULT_FILE_STORAGE -
docs/releases/1.4.txt
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index bddb15c..a0eb55a 100644
a b help with AJAX heavy sites, protection for PUT and DELETE, and settings 112 112 the security and usefulness of the CSRF protection. See the :doc:`CSRF docs 113 113 </ref/contrib/csrf>` for more information. 114 114 115 Error report filtering 116 ~~~~~~~~~~~~~~~~~~~~~~ 117 118 Two new function decorators, :func:`sensitive_variables` and 119 :func:`sensitive_post_parameters`, were added to allow designating the 120 traceback frames' local variables and request's POST parameters susceptible 121 to contain sensitive information and that should be filtered out of error 122 reports. 123 124 All POST parameters are now systematically filtered out of error reports for 125 certain :mod:`contrib.views.auth` views (``login``, ``password_reset_confirm``, 126 ``password_change``, and ``add_view`` and ``user_change_password`` in the 127 ``auth`` admin) to prevent the leaking of sensitive information such as user 128 passwords. 129 130 You may override or customize the default filtering by writing a 131 :ref:`custom filter<custom-error-reports>`. Learn more on 132 :ref:`Filtering error reports<filtering-error-reports>`. 133 134 115 135 .. _backwards-incompatible-changes-1.4: 116 136 117 137 Backwards incompatible changes in 1.4 -
docs/topics/logging.txt
diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 651a92f..e2c9f72 100644
a b Python logging module. 504 504 sensitive, and you may not want to send it over email. Consider using 505 505 something such as `django-sentry`_ to get the best of both worlds -- the 506 506 rich information of full tracebacks plus the security of *not* sending the 507 information over email. 507 information over email. You may also explicitly designate certain 508 sensitive information to be filtered out of error reports -- learn more on 509 :ref:`Filtering error reports<filtering-error-reports>`. 508 510 509 511 .. _django-sentry: http://pypi.python.org/pypi/django-sentry -
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 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, 17 paranoid_view) 13 18 14 19 15 20 class DebugViewTests(TestCase): … … class ExceptionReporterTests(TestCase): 143 148 self.assertNotIn('<h2>Traceback ', html) 144 149 self.assertIn('<h2>Request information</h2>', html) 145 150 self.assertIn('<p>Request data not supplied</p>', html) 151 152 153 class 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..29da20f 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.debug import (sensitive_post_parameters, 10 sensitive_variables) 11 from django.utils.log import getLogger 9 12 10 13 from regressiontests.views import BrokenException, except_args 11 14 … … def raises_template_does_not_exist(request): 128 131 return render_to_response('i_dont_exist.html') 129 132 except TemplateDoesNotExist: 130 133 return technical_500_response(request, *sys.exc_info()) 134 135 def 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 145 def 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') 160 def 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() 175 def 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)