diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 288dc9a..1300e86 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -537,6 +537,10 @@ LOGGING = {
     }
 }
 
+# Default exception reporter filter class used in case none has been
+# specifically assigned to the HttpRequest instance.
+DEFAULT_EXCEPTION_REPORTER_FILTER = 'django.views.debug.SafeExceptionReporterFilter'
+
 ###########
 # TESTING #
 ###########
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index 7d855d8..237b153 100644
--- a/django/contrib/auth/admin.py
+++ b/django/contrib/auth/admin.py
@@ -12,6 +12,7 @@ from django.utils.html import escape
 from django.utils.decorators import method_decorator
 from django.utils.translation import ugettext, ugettext_lazy as _
 from django.views.decorators.csrf import csrf_protect
+from django.views.decorators.debug import sensitive_post_parameters
 
 csrf_protect_m = method_decorator(csrf_protect)
 
@@ -78,6 +79,7 @@ class UserAdmin(admin.ModelAdmin):
             (r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password))
         ) + super(UserAdmin, self).get_urls()
 
+    @sensitive_post_parameters()
     @csrf_protect_m
     @transaction.commit_on_success
     def add_view(self, request, form_url='', extra_context=None):
@@ -102,6 +104,7 @@ class UserAdmin(admin.ModelAdmin):
         extra_context.update(defaults)
         return super(UserAdmin, self).add_view(request, form_url, extra_context)
 
+    @sensitive_post_parameters()
     def user_change_password(self, request, id):
         if not self.has_change_permission(request):
             raise PermissionDenied
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index cfb2659..6a97ffa 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -6,6 +6,7 @@ from django.http import HttpResponseRedirect, QueryDict
 from django.template.response import TemplateResponse
 from django.utils.http import base36_to_int
 from django.utils.translation import ugettext as _
+from django.views.decorators.debug import sensitive_post_parameters
 from django.views.decorators.cache import never_cache
 from django.views.decorators.csrf import csrf_protect
 
@@ -17,7 +18,7 @@ from django.contrib.auth.models import User
 from django.contrib.auth.tokens import default_token_generator
 from django.contrib.sites.models import get_current_site
 
-
+@sensitive_post_parameters()
 @csrf_protect
 @never_cache
 def login(request, template_name='registration/login.html',
@@ -175,6 +176,7 @@ def password_reset_done(request,
                             current_app=current_app)
 
 # Doesn't need csrf_protect since no-one can guess the URL
+@sensitive_post_parameters()
 @never_cache
 def password_reset_confirm(request, uidb36=None, token=None,
                            template_name='registration/password_reset_confirm.html',
@@ -227,6 +229,7 @@ def password_reset_complete(request,
     return TemplateResponse(request, template_name, context,
                             current_app=current_app)
 
+@sensitive_post_parameters()
 @csrf_protect
 @login_required
 def password_change(request,
diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
index d653860..7cae42e 100644
--- a/django/core/handlers/base.py
+++ b/django/core/handlers/base.py
@@ -206,7 +206,7 @@ class BaseHandler(object):
             exc_info=exc_info,
             extra={
                 'status_code': 500,
-                'request':request
+                'request': request
             }
         )
 
diff --git a/django/utils/log.py b/django/utils/log.py
index 93e38d1..c1cd328 100644
--- a/django/utils/log.py
+++ b/django/utils/log.py
@@ -1,6 +1,10 @@
 import logging
 import sys
+import traceback
+
+from django.conf import settings
 from django.core import mail
+from django.views.debug import ExceptionReporter, get_exception_reporter_filter
 
 # Make sure a NullHandler is available
 # This was added in Python 2.7/3.2
@@ -35,13 +39,9 @@ class AdminEmailHandler(logging.Handler):
     """An exception log handler that emails log entries to site admins.
 
     If the request is passed as the first argument to the log record,
-    request data will be provided in the
+    request data will be provided in the email report.
     """
     def emit(self, record):
-        import traceback
-        from django.conf import settings
-        from django.views.debug import ExceptionReporter
-
         try:
             request = record.request
             subject = '%s (%s IP): %s' % (
@@ -49,16 +49,16 @@ class AdminEmailHandler(logging.Handler):
                 (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'),
                 record.msg
             )
-            request_repr = repr(request)
+            filter = get_exception_reporter_filter(request)
+            request_repr = filter.get_request_repr(request)
         except:
             subject = '%s: %s' % (
                 record.levelname,
                 record.msg
             )
-
             request = None
-            request_repr = "Request repr() unavailable"
-
+            request_repr = "Request repr() unavailable."
+                
         if record.exc_info:
             exc_info = record.exc_info
             stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
diff --git a/django/views/debug.py b/django/views/debug.py
index 67f25b3..5e3900c 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -3,9 +3,11 @@ import os
 import re
 import sys
 import types
+from pprint import pformat
 
 from django.conf import settings
-from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound
+from django.http import (HttpResponse, HttpResponseServerError,
+    HttpResponseNotFound, HttpRequest)
 from django.template import (Template, Context, TemplateDoesNotExist,
     TemplateSyntaxError)
 from django.template.defaultfilters import force_escape, pprint
@@ -15,6 +17,8 @@ from django.utils.encoding import smart_unicode, smart_str
 
 HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD|PROFANITIES_LIST|SIGNATURE')
 
+CLEANSED_SUBSTITUTE = u'********************'
+
 def linebreak_iter(template_source):
     yield 0
     p = template_source.find('\n')
@@ -31,7 +35,7 @@ def cleanse_setting(key, value):
     """
     try:
         if HIDDEN_SETTINGS.search(key):
-            cleansed = '********************'
+            cleansed = CLEANSED_SUBSTITUTE
         else:
             if isinstance(value, dict):
                 cleansed = dict((k, cleanse_setting(k, v)) for k,v in value.items())
@@ -59,12 +63,158 @@ def technical_500_response(request, exc_type, exc_value, tb):
     html = reporter.get_traceback_html()
     return HttpResponseServerError(html, mimetype='text/html')
 
+# Cache for the default exception reporter filter instance.
+default_exception_reporter_filter = None
+
+def get_exception_reporter_filter(request):
+    global default_exception_reporter_filter
+    if default_exception_reporter_filter is None:
+        # Load the default filter for the first time and cache it.
+        modpath = settings.DEFAULT_EXCEPTION_REPORTER_FILTER
+        modname, classname = modpath.rsplit('.', 1)
+        try:
+            mod = import_module(modname)
+        except ImportError, e:
+            raise ImproperlyConfigured(
+            'Error importing default exception reporter filter %s: "%s"' % (modpath, e))
+        try:
+            default_exception_reporter_filter = getattr(mod, classname)()
+        except AttributeError:
+            raise exceptions.ImproperlyConfigured('Default exception reporter filter module "%s" does not define a "%s" class' % (modname, classname))
+    if request:
+        return getattr(request, 'exception_reporter_filter', default_exception_reporter_filter)
+    else:
+        return default_exception_reporter_filter
+
+class ExceptionReporterFilter(object):
+    """
+    Base for all exception reporter filter classes. All overridable hooks
+    contain lenient default behaviours.
+    """
+    
+    def get_request_repr(self, request):
+        if request is None:
+            return repr(None)
+        else:
+            # Since this is called as part of error handling, we need to be very
+            # robust against potentially malformed input.
+            try:
+                get = pformat(request.GET)
+            except:
+                get = '<could not parse>'
+            if request._post_parse_error:
+                post = '<could not parse>'
+            else:
+                try:
+                    post = pformat(self.get_post_parameters(request))
+                except:
+                    post = '<could not parse>'
+            try:
+                cookies = pformat(request.COOKIES)
+            except:
+                cookies = '<could not parse>'
+            try:
+                meta = pformat(request.META)
+            except:
+                meta = '<could not parse>'
+            return smart_str(u'<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' %
+                             (request.__class__.__name__,
+                              request.path,
+                              unicode(get),
+                              unicode(post),
+                              unicode(cookies),
+                              unicode(meta)))
+
+    def get_post_parameters(self, request):
+        if request is None:
+            return {}
+        else:
+            return request.POST
+            
+    def get_traceback_frame_variables(self, request, tb_frame):
+        return tb_frame.f_locals.items()
+
+class SafeExceptionReporterFilter(ExceptionReporterFilter):
+    """
+    Use annotations made by the sensitive_post_parameters and
+    sensitive_variables decorators to filter out sensitive information.
+    """
+    
+    def is_active(self, request):
+        """
+        This filter is to add safety in production environments (i.e. DEBUG
+        is False). If DEBUG is True then your site is not safe anyway.
+        This hook is provided as a convenience to easily activate or
+        deactivate the filter on a per request basis. 
+        """
+        return settings.DEBUG is False
+    
+    def get_post_parameters(self, request):
+        """
+        Replaces the values of POST parameters marked as sensitive with
+        stars (*********).
+        """
+        if request is None:
+            return {}
+        else:
+            sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
+            if self.is_active(request) and sensitive_post_parameters:
+                cleansed = request.POST.copy()
+                if sensitive_post_parameters == '__ALL__':
+                    # Cleanse all parameters.
+                    for k, v in cleansed.items():
+                        cleansed[k] = CLEANSED_SUBSTITUTE
+                    return cleansed
+                else:
+                    # Cleanse only the specified parameters.
+                    for param in sensitive_post_parameters:
+                        if cleansed.has_key(param):
+                            cleansed[param] = CLEANSED_SUBSTITUTE
+                    return cleansed
+            else:
+                return request.POST
+    
+    def get_traceback_frame_variables(self, request, tb_frame):
+        """
+        Replaces the values of variables marked as sensitive with
+        stars (*********).
+        """
+        func_name = tb_frame.f_code.co_name
+        func = tb_frame.f_globals.get(func_name)
+        sensitive_variables = getattr(func, 'sensitive_variables', [])
+        cleansed = []
+        if self.is_active(request) and sensitive_variables:
+            if sensitive_variables == '__ALL__':
+                # Cleanse all variables
+                for name, value in tb_frame.f_locals.items():
+                    cleansed.append((name, CLEANSED_SUBSTITUTE))
+                return cleansed
+            else:
+                # Cleanse specified variables
+                for name, value in tb_frame.f_locals.items():
+                    if name in sensitive_variables:
+                        value = CLEANSED_SUBSTITUTE
+                    elif isinstance(value, HttpRequest):
+                        # Cleanse the request's POST parameters.
+                        value = self.get_request_repr(value)
+                    cleansed.append((name, value))
+                return cleansed
+        else:
+            # Potentially cleanse only the request if it's one of the frame variables.
+            for name, value in tb_frame.f_locals.items():
+                if isinstance(value, HttpRequest):
+                    # Cleanse the request's POST parameters.
+                    value = self.get_request_repr(value)
+                cleansed.append((name, value))
+            return cleansed
+    
 class ExceptionReporter(object):
     """
     A class to organize and coordinate reporting on exceptions.
     """
     def __init__(self, request, exc_type, exc_value, tb, is_email=False):
         self.request = request
+        self.filter = get_exception_reporter_filter(self.request)
         self.exc_type = exc_type
         self.exc_value = exc_value
         self.tb = tb
@@ -124,6 +274,7 @@ class ExceptionReporter(object):
             'unicode_hint': unicode_hint,
             'frames': frames,
             'request': self.request,
+            'filtered_POST': self.filter.get_post_parameters(self.request),
             'settings': get_safe_settings(),
             'sys_executable': sys.executable,
             'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
@@ -222,7 +373,7 @@ class ExceptionReporter(object):
         frames = []
         tb = self.tb
         while tb is not None:
-            # support for __traceback_hide__ which is used by a few libraries
+            # Support for __traceback_hide__ which is used by a few libraries
             # to hide internal frames.
             if tb.tb_frame.f_locals.get('__traceback_hide__'):
                 tb = tb.tb_next
@@ -239,7 +390,7 @@ class ExceptionReporter(object):
                     'filename': filename,
                     'function': function,
                     'lineno': lineno + 1,
-                    'vars': tb.tb_frame.f_locals.items(),
+                    'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame),
                     'id': id(tb),
                     'pre_context': pre_context,
                     'context_line': context_line,
@@ -247,7 +398,6 @@ class ExceptionReporter(object):
                     'pre_context_lineno': pre_context_lineno + 1,
                 })
             tb = tb.tb_next
-
         return frames
 
     def format_exception(self):
@@ -643,7 +793,7 @@ Exception Value: {{ exception_value|force_escape }}
   {% endif %}
 
   <h3 id="post-info">POST</h3>
-  {% if request.POST %}
+  {% if filtered_POST %}
     <table class="req">
       <thead>
         <tr>
@@ -652,7 +802,7 @@ Exception Value: {{ exception_value|force_escape }}
         </tr>
       </thead>
       <tbody>
-        {% for var in request.POST.items %}
+        {% for var in filtered_POST.items %}
           <tr>
             <td>{{ var.0 }}</td>
             <td class="code"><pre>{{ var.1|pprint }}</pre></td>
diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py
new file mode 100644
index 0000000..9974629
--- /dev/null
+++ b/django/views/decorators/debug.py
@@ -0,0 +1,71 @@
+import functools
+
+
+def sensitive_variables(*variables):
+    """
+    Indicates which variables used in the decorated function are sensitive, so
+    that those variables can later be treated in a special way, for example
+    by hiding them when logging unhandled exceptions.
+    
+    Two forms are accepted:
+    
+    * with specified variable names:
+    
+        @sensitive_variables('user', 'password', 'credit_card')
+        def my_function(user):
+            password = user.pass_word
+            credit_card = user.credit_card_number
+            ...
+            
+    * without any specified variable names, in which case it is assumed that
+      all variables are considered sensitive:
+      
+        @sensitive_variables()
+        def my_function()
+            ...
+    """
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapper(*args, **kwargs):
+            if variables:
+                wrapper.sensitive_variables = variables
+            else:
+                wrapper.sensitive_variables = '__ALL__'
+            return func(*args, **kwargs)
+        return wrapper
+    return decorator
+
+
+def sensitive_post_parameters(*parameters):
+    """
+    Indicates which POST parameters used in the decorated view are sensitive,
+    so that those parameters can later be treated in a special way, for example
+    by hiding them when logging unhandled exceptions.
+    
+    Two forms are accepted:
+    
+    * with specified parameters:
+    
+        @sensitive_post_parameters('password', 'credit_card')
+        def my_view(request):
+            pw = request.POST['password']
+            cc = request.POST['credit_card']
+            ...
+            
+    * without any specified parameters, in which case it is assumed that
+      all parameters are considered sensitive:
+      
+        @sensitive_post_parameters()
+        def my_view(request)
+            ...
+    """
+    def decorator(view):
+        @functools.wraps(view)
+        def wrapper(request, *args, **kwargs):
+            if parameters:
+                request.sensitive_post_parameters = parameters
+            else:
+                request.sensitive_post_parameters = '__ALL__'
+            return view(request, *args, **kwargs)
+        return wrapper
+    return decorator
diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt
index c15c1d8..f87c57c 100644
--- a/docs/howto/error-reporting.txt
+++ b/docs/howto/error-reporting.txt
@@ -1,5 +1,5 @@
-Error reporting via email
-=========================
+Error reporting
+===============
 
 When you're running a public site you should always turn off the
 :setting:`DEBUG` setting. That will make your server run much faster, and will
@@ -9,11 +9,14 @@ revealed by the error pages.
 However, running with :setting:`DEBUG` set to ``False`` means you'll never see
 errors generated by your site -- everyone will just see your public error pages.
 You need to keep track of errors that occur in deployed sites, so Django can be
-configured to email you details of those errors.
+configured to create reports with details about those errors.
 
-Server errors
+Email reports
 -------------
 
+Server errors
+~~~~~~~~~~~~~
+
 When :setting:`DEBUG` is ``False``, Django will email the users listed in the
 :setting:`ADMINS` setting whenever your code raises an unhandled exception and
 results in an internal server error (HTTP status code 500). This gives the
@@ -48,7 +51,7 @@ setting.
    </topics/logging>`.
 
 404 errors
-----------
+~~~~~~~~~~
 
 Django can also be configured to email errors about broken links (404 "page
 not found" errors). Django sends emails about 404 errors when:
@@ -96,13 +99,6 @@ The best way to disable this behavior is to set
 
 .. seealso::
 
-    You can also set up custom error reporting by writing a custom piece of
-    :ref:`exception middleware <exception-middleware>`. If you do write custom
-    error handling, it's a good idea to emulate Django's built-in error handling
-    and only report/log errors if :setting:`DEBUG` is ``False``.
-
-.. seealso::
-
    .. versionadded:: 1.3
 
    404 errors are logged using the logging framework. By default, these log
@@ -116,3 +112,174 @@ The best way to disable this behavior is to set
    Previously, two settings were used to control which URLs not to report:
    :setting:`IGNORABLE_404_STARTS` and :setting:`IGNORABLE_404_ENDS`. They
    were replaced by :setting:`IGNORABLE_404_URLS`.
+
+.. _filtering-error-reports:
+
+Filtering error reports
+-----------------------
+
+.. versionadded:: 1.4
+
+Filtering sensitive information
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error reports are really helpful for debugging errors, so it is generally
+useful to record as much relevant information about those errors as possible.
+For example, by default Django records the `full traceback`_ for the
+exception raised, each `traceback frame`_'s local variables, and the
+:class:`HttpRequest`'s :ref:`attributes<httprequest-attributes>`.
+
+However, sometimes certain types of information may be too sensitive and thus
+may not be appropriate to be kept track of, for example a user's password or
+credit card number. So Django offers a set of function decorators to help you
+control which information should be filtered out of error reports in a
+production environment (that is, where :setting:`DEBUG` is set to ``False``):
+:func:`sensitive_variables` and :func:`sensitive_post_parameters`.
+
+.. _`full traceback`: http://en.wikipedia.org/wiki/Stack_trace
+.. _`traceback frame`: http://en.wikipedia.org/wiki/Stack_frame
+
+.. function:: sensitive_variables(*variables)
+
+    If a function (either a view or any regular callback) in your code uses
+    local variables susceptible to contain sensitive information, you may
+    prevent the values of those variables from being included in error reports
+    using the ``sensitive_variables`` decorator:
+    
+        .. code-block:: python
+        
+            from django.views.decorators.debug import sensitive_variables 
+        
+            @sensitive_variables('user', 'pw', 'cc')
+            def process_info(user):
+                pw = user.pass_word
+                cc = user.credit_card_number
+                name = user.name
+                ...
+                    
+    In the above example, the values for the ``user``, ``pw`` and ``cc``
+    variables will be hidden and replaced with stars (`**********`) in the
+    error reports, whereas the value of the ``name`` variable will be
+    disclosed.
+
+    To systematically hide all local variables of a function from error logs,
+    do not provide any argument to the ``sensitive_variables`` decorator:
+    
+        .. code-block:: python
+
+            @sensitive_variables()
+            def my_function():
+                ...
+
+.. function:: sensitive_post_parameters(*parameters)
+
+    If one of your views receives an :class:`HttpRequest` object with
+    :attr:`POST parameters<HttpRequest.POST>` susceptible to contain sensitive
+    information, you may prevent the values of those parameters from being
+    included in the error reports using the ``sensitive_post_parameters``
+    decorator:
+    
+        .. code-block:: python
+        
+            from django.views.decorators.debug import sensitive_post_parameters
+        
+            @sensitive_post_parameters('pass_word', 'credit_card_number')
+            def record_user_profile(request):
+                UserProfile.create(user=request.user,
+                                   password=request.POST['pass_word'],
+                                   credit_card=request.POST['credit_card_number'],
+                                   name=request.POST['name'])
+                ...
+                
+    In the above example, the values for the ``pass_word`` and
+    ``credit_card_number`` POST parameters will be hidden and replaced with
+    stars (`**********`) in the request's representation inside the error
+    reports, whereas the value of the ``name`` parameter will be disclosed.
+
+    To systematically hide all POST parameters of a request in error reports,
+    do not provide any argument to the ``sensitive_post_parameters`` decorator:
+    
+        .. code-block:: python
+
+            @sensitive_post_parameters()
+            def my_view(request):
+                ...
+
+.. note::
+
+    .. versionchanged:: 1.4
+    
+    Since version 1.4, all POST parameters are systematically filtered out of
+    error reports for certain :mod:`contrib.views.auth` views (``login``,
+    ``password_reset_confirm``, ``password_change``, and ``add_view`` and
+    ``user_change_password`` in the ``auth`` admin) to prevent the leaking of
+    sensitive information such as user passwords.
+
+.. _custom-error-reports:
+
+Custom error reports
+~~~~~~~~~~~~~~~~~~~~
+
+All :func:`sensitive_variables` and :func:`sensitive_post_parameters` do is,
+respectively, annotate the decorated function with the names of sensitive
+variables and annotate the ``HttpRequest`` object with the names of sensitive
+POST parameters, so that this sensitive information can later be filtered out
+of reports when an error occurs. The actual filtering is done by Django's
+default error reporter filter:
+:class:`django.views.debug.SafeExceptionReporterFilter`. This filter uses the
+decorators' annotations to replace the corresponding values with stars
+(`**********`) when the error reports are produced. If you wish to override or
+customize this default behavior for your entire site, you need to define your
+own filter class and tell Django to use it via the
+:setting:`DEFAULT_EXCEPTION_REPORTER_FILTER` setting:
+
+    .. code-block:: python
+    
+        DEFAULT_EXCEPTION_REPORTER_FILTER = 'path.to.your.CustomExceptionReporterFilter'
+
+You may also control in a more granular way which filter to use within any
+given view by setting the ``HttpRequest``'s ``exception_reporter_filter``
+attribute:
+
+    .. code-block:: python
+
+        def my_view(request):
+            if request.user.is_authenticated():
+                request.exception_reporter_filter = CustomExceptionReporterFilter()
+            ...
+
+Your custom filter class needs to inherit from
+:class:`django.views.debug.SafeExceptionReporterFilter` and may override the
+following methods:
+
+.. class:: django.views.debug.SafeExceptionReporterFilter
+    
+.. method:: SafeExceptionReporterFilter.is_active(self, request)
+
+    Returns ``True`` to activate the filtering operated in the other methods.
+    By default the filter is active if :setting:`DEBUG` is ``False``.
+    
+.. method:: SafeExceptionReporterFilter.get_request_repr(self, request)
+
+    Returns the representation string of the request object, that is, the
+    value that would be returned by ``repr(request)``, except it uses the
+    filtered dictionary of POST parameters as determined by
+    :meth:`SafeExceptionReporterFilter.get_post_parameters`.
+
+.. method:: SafeExceptionReporterFilter.get_post_parameters(self, request)
+
+    Returns the filtered dictionary of POST parameters. By default it replaces
+    the values of sensitive parameters with stars (`**********`).
+    
+.. method:: SafeExceptionReporterFilter.get_traceback_frame_variables(self, request, tb_frame)
+
+    Returns the filtered dictionary of local variables for the given traceback
+    frame. By default it replaces the values of sensitive variables with stars
+    (`**********`).
+
+.. seealso::
+
+    You can also set up custom error reporting by writing a custom piece of
+    :ref:`exception middleware <exception-middleware>`. If you do write custom
+    error handling, it's a good idea to emulate Django's built-in error handling
+    and only report/log errors if :setting:`DEBUG` is ``False``.
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
index 72872d5..ff04e25 100644
--- a/docs/ref/request-response.txt
+++ b/docs/ref/request-response.txt
@@ -23,6 +23,8 @@ HttpRequest objects
 
 .. class:: HttpRequest
 
+.. _httprequest-attributes:
+
 Attributes
 ----------
 
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 816c3e9..e95c165 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -772,6 +772,17 @@ Default content type to use for all ``HttpResponse`` objects, if a MIME type
 isn't manually specified. Used with :setting:`DEFAULT_CHARSET` to construct
 the ``Content-Type`` header.
 
+.. setting:: DEFAULT_EXCEPTION_REPORTER_FILTER
+
+DEFAULT_EXCEPTION_REPORTER_FILTER
+---------------------------------
+
+Default: :class:`django.views.debug.SafeExceptionReporterFilter`
+
+Default exception reporter filter class to be used if none has been assigned to
+the :class:`HttpRequest` instance yet.
+See :ref:`Filtering error reports<filtering-error-reports>`.
+
 .. setting:: DEFAULT_FILE_STORAGE
 
 DEFAULT_FILE_STORAGE
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
index bddb15c..a0eb55a 100644
--- a/docs/releases/1.4.txt
+++ b/docs/releases/1.4.txt
@@ -112,6 +112,26 @@ help with AJAX heavy sites, protection for PUT and DELETE, and settings
 the security and usefulness of the CSRF protection. See the :doc:`CSRF docs
 </ref/contrib/csrf>` for more information.
 
+Error report filtering
+~~~~~~~~~~~~~~~~~~~~~~
+
+Two new function decorators, :func:`sensitive_variables` and
+:func:`sensitive_post_parameters`, were added to allow designating the
+traceback frames' local variables and request's POST parameters susceptible
+to contain sensitive information and that should be filtered out of error
+reports. 
+
+All POST parameters are now systematically filtered out of error reports for
+certain :mod:`contrib.views.auth` views (``login``, ``password_reset_confirm``,
+``password_change``, and ``add_view`` and ``user_change_password`` in the
+``auth`` admin) to prevent the leaking of sensitive information such as user
+passwords.
+
+You may override or customize the default filtering by writing a
+:ref:`custom filter<custom-error-reports>`. Learn more on
+:ref:`Filtering error reports<filtering-error-reports>`.
+
+
 .. _backwards-incompatible-changes-1.4:
 
 Backwards incompatible changes in 1.4
diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt
index 651a92f..e2c9f72 100644
--- a/docs/topics/logging.txt
+++ b/docs/topics/logging.txt
@@ -504,6 +504,8 @@ Python logging module.
     sensitive, and you may not want to send it over email. Consider using
     something such as `django-sentry`_ to get the best of both worlds -- the
     rich information of full tracebacks plus the security of *not* sending the
-    information over email.
+    information over email. You may also explicitly designate certain
+    sensitive information to be filtered out of error reports -- learn more on
+    :ref:`Filtering error reports<filtering-error-reports>`.
 
 .. _django-sentry: http://pypi.python.org/pypi/django-sentry
diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py
index 6dd4bd4..da71aaf 100644
--- a/tests/regressiontests/views/tests/debug.py
+++ b/tests/regressiontests/views/tests/debug.py
@@ -1,3 +1,4 @@
+from __future__ import with_statement
 import inspect
 import os
 import sys
@@ -8,8 +9,12 @@ from django.test import TestCase, RequestFactory
 from django.core.urlresolvers import reverse
 from django.template import TemplateSyntaxError
 from django.views.debug import ExceptionReporter
+from django.core.exceptions import ImproperlyConfigured
+from django.core import mail
 
 from regressiontests.views import BrokenException, except_args
+from regressiontests.views.views import (sensitive_view, non_sensitive_view,
+    paranoid_view, custom_exception_reporter_filter_view)
 
 
 class DebugViewTests(TestCase):
@@ -143,3 +148,188 @@ class ExceptionReporterTests(TestCase):
         self.assertNotIn('<h2>Traceback ', html)
         self.assertIn('<h2>Request information</h2>', html)
         self.assertIn('<p>Request data not supplied</p>', html)
+
+
+class ExceptionReporterFilterTests(TestCase):
+    """
+    Ensure that sensitive information can be filtered out of error reports.
+    Refs #14614.
+    """
+    rf = RequestFactory()
+    breakfast_data = {'sausage-key': 'sausage-value',
+                      'baked-beans-key': 'baked-beans-value',
+                      'hash-brown-key': 'hash-brown-value',
+                      'bacon-key': 'bacon-value',}
+           
+    def verify_unsafe_response(self, view):
+        """
+        Asserts that potentially sensitive info are displayed in the response.
+        """
+        request = self.rf.post('/some_url/', self.breakfast_data)
+        response = view(request)
+        # All variables are shown.
+        self.assertContains(response, 'cooked_eggs', status_code=500)
+        self.assertContains(response, 'scrambled', status_code=500)
+        self.assertContains(response, 'sauce', status_code=500)
+        self.assertContains(response, 'worcestershire', status_code=500)
+        for k, v in self.breakfast_data.items():
+            # All POST parameters are shown.
+            self.assertContains(response, k, status_code=500)
+            self.assertContains(response, v, status_code=500)
+    
+    def verify_safe_response(self, view):
+        """
+        Asserts that certain sensitive info are not displayed in the response.
+        """
+        request = self.rf.post('/some_url/', self.breakfast_data)
+        response = view(request)
+        # Non-sensitive variable's name and value are shown.
+        self.assertContains(response, 'cooked_eggs', status_code=500)
+        self.assertContains(response, 'scrambled', status_code=500)
+        # Sensitive variable's name is shown but not its value.
+        self.assertContains(response, 'sauce', status_code=500)
+        self.assertNotContains(response, 'worcestershire', status_code=500)
+        for k, v in self.breakfast_data.items():
+            # All POST parameters' names are shown.
+            self.assertContains(response, k, status_code=500)
+        # Non-sensitive POST parameters' values are shown.
+        self.assertContains(response, 'baked-beans-value', status_code=500)
+        self.assertContains(response, 'hash-brown-value', status_code=500)
+        # Sensitive POST parameters' values are not shown.
+        self.assertNotContains(response, 'sausage-value', status_code=500)
+        self.assertNotContains(response, 'bacon-value', status_code=500)
+    
+    def verify_paranoid_response(self, view):
+        """
+        Asserts that no variables or POST parameters are displayed in the response.
+        """
+        request = self.rf.post('/some_url/', self.breakfast_data)
+        response = view(request)
+        # Show variable names but not their values.
+        self.assertContains(response, 'cooked_eggs', status_code=500)
+        self.assertNotContains(response, 'scrambled', status_code=500)
+        self.assertContains(response, 'sauce', status_code=500)
+        self.assertNotContains(response, 'worcestershire', status_code=500)
+        for k, v in self.breakfast_data.items():
+            # All POST parameters' names are shown.
+            self.assertContains(response, k, status_code=500)
+            # No POST parameters' values are shown.
+            self.assertNotContains(response, v, status_code=500)
+        
+    def verify_unsafe_email(self, view):
+        """
+        Asserts that potentially sensitive info are displayed in the email report.
+        """
+        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
+            mail.outbox = [] # Empty outbox
+            request = self.rf.post('/some_url/', self.breakfast_data)
+            response = view(request) 
+            self.assertEquals(len(mail.outbox), 1)
+            email = mail.outbox[0]
+            # Frames vars are never shown in plain text email reports.
+            self.assertNotIn('cooked_eggs', email.body)
+            self.assertNotIn('scrambled', email.body)
+            self.assertNotIn('sauce', email.body)
+            self.assertNotIn('worcestershire', email.body)
+            for k, v in self.breakfast_data.items():
+                # All POST parameters are shown.
+                self.assertIn(k, email.body)
+                self.assertIn(v, email.body)
+        
+    def verify_safe_email(self, view):
+        """
+        Asserts that certain sensitive info are not displayed in the email report.
+        """
+        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
+            mail.outbox = [] # Empty outbox
+            request = self.rf.post('/some_url/', self.breakfast_data)
+            response = view(request)
+            self.assertEquals(len(mail.outbox), 1)
+            email = mail.outbox[0]
+            # Frames vars are never shown in plain text email reports.
+            self.assertNotIn('cooked_eggs', email.body)
+            self.assertNotIn('scrambled', email.body)
+            self.assertNotIn('sauce', email.body)
+            self.assertNotIn('worcestershire', email.body)
+            for k, v in self.breakfast_data.items():
+                # All POST parameters' names are shown.
+                self.assertIn(k, email.body)
+            # Non-sensitive POST parameters' values are shown.
+            self.assertIn('baked-beans-value', email.body)
+            self.assertIn('hash-brown-value', email.body)
+            # Sensitive POST parameters' values are not shown.
+            self.assertNotIn('sausage-value', email.body)
+            self.assertNotIn('bacon-value', email.body)
+    
+    def verify_paranoid_email(self, view):
+        """
+        Asserts that no variables or POST parameters are displayed in the email report.
+        """
+        with self.settings(ADMINS=(('Admin', 'admin@fattie-breakie.com'),)):
+            mail.outbox = [] # Empty outbox
+            request = self.rf.post('/some_url/', self.breakfast_data)
+            response = view(request)
+            self.assertEquals(len(mail.outbox), 1)
+            email = mail.outbox[0]
+            # Frames vars are never shown in plain text email reports.
+            self.assertNotIn('cooked_eggs', email.body)
+            self.assertNotIn('scrambled', email.body)
+            self.assertNotIn('sauce', email.body)
+            self.assertNotIn('worcestershire', email.body)
+            for k, v in self.breakfast_data.items():
+                # All POST parameters' names are shown.
+                self.assertIn(k, email.body)
+                # No POST parameters' values are shown.
+                self.assertNotIn(v, email.body)
+            
+    def test_non_sensitive_request(self):
+        """
+        Ensure that everything (request info and frame variables) can bee seen
+        in the default error reports for non-sensitive requests.
+        """ 
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(non_sensitive_view)
+            self.verify_unsafe_email(non_sensitive_view)
+                
+        with self.settings(DEBUG=False):
+            self.verify_unsafe_response(non_sensitive_view)
+            self.verify_unsafe_email(non_sensitive_view)
+        
+    def test_sensitive_request(self):
+        """
+        Ensure that sensitive POST parameters and frame variables cannot be
+        seen in the default error reports for sensitive requests.
+        """
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(sensitive_view)
+            self.verify_unsafe_email(sensitive_view)
+            
+        with self.settings(DEBUG=False):
+            self.verify_safe_response(sensitive_view)
+            self.verify_safe_email(sensitive_view)
+    
+    def test_paranoid_request(self):
+        """
+        Ensure that no POST parameters and frame variables can be seen in the
+        default error reports for "paranoid" requests.
+        """
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(paranoid_view)
+            self.verify_unsafe_email(paranoid_view)
+            
+        with self.settings(DEBUG=False):
+            self.verify_paranoid_response(paranoid_view)
+            self.verify_paranoid_email(paranoid_view)
+
+    def test_custom_exception_reporter_filter(self):
+        """
+        Ensure that it's possible to assign an exception reporter filter to
+        the request to bypass the one set in DEFAULT_EXCEPTION_REPORTER_FILTER.
+        """ 
+        with self.settings(DEBUG=True):
+            self.verify_unsafe_response(custom_exception_reporter_filter_view)
+            self.verify_unsafe_email(custom_exception_reporter_filter_view)
+                
+        with self.settings(DEBUG=False):
+            self.verify_unsafe_response(custom_exception_reporter_filter_view)
+            self.verify_unsafe_email(custom_exception_reporter_filter_view)
diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py
index 11d289f..6c2f324 100644
--- a/tests/regressiontests/views/views.py
+++ b/tests/regressiontests/views/views.py
@@ -5,7 +5,10 @@ from django.http import HttpResponse, HttpResponseRedirect
 from django.core.urlresolvers import get_resolver
 from django.shortcuts import render_to_response, render
 from django.template import Context, RequestContext, TemplateDoesNotExist
-from django.views.debug import technical_500_response
+from django.views.debug import technical_500_response, SafeExceptionReporterFilter
+from django.views.decorators.debug import (sensitive_post_parameters,
+                                           sensitive_variables)
+from django.utils.log import getLogger
 
 from regressiontests.views import BrokenException, except_args
 
@@ -128,3 +131,84 @@ def raises_template_does_not_exist(request):
         return render_to_response('i_dont_exist.html')
     except TemplateDoesNotExist:
         return technical_500_response(request, *sys.exc_info())
+
+def send_log(request, exc_info):
+    logger = getLogger('django.request')
+    logger.error('Internal Server Error: %s' % request.path,
+        exc_info=exc_info,
+        extra={
+            'status_code': 500,
+            'request': request
+        }
+    )
+
+def non_sensitive_view(request):
+    # Do not just use plain strings for the variables' values in the code
+    # so that the tests don't return false positives when the function's source
+    # is displayed in the exception report. 
+    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
+    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
+    try:
+        raise Exception
+    except Exception:
+        exc_info = sys.exc_info()
+        send_log(request, exc_info)
+        return technical_500_response(request, *exc_info)
+    
+@sensitive_variables('sauce')
+@sensitive_post_parameters('bacon-key', 'sausage-key')
+def sensitive_view(request):
+    # Do not just use plain strings for the variables' values in the code
+    # so that the tests don't return false positives when the function's source
+    # is displayed in the exception report.
+    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
+    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
+    try:
+        raise Exception
+    except Exception:
+        exc_info = sys.exc_info()
+        send_log(request, exc_info)
+        return technical_500_response(request, *exc_info)
+
+@sensitive_variables()
+@sensitive_post_parameters()
+def paranoid_view(request):
+    # Do not just use plain strings for the variables' values in the code
+    # so that the tests don't return false positives when the function's source
+    # is displayed in the exception report.
+    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
+    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
+    try:
+        raise Exception
+    except Exception:
+        exc_info = sys.exc_info()
+        send_log(request, exc_info)
+        return technical_500_response(request, *exc_info)
+
+class UnsafeExceptionReporterFilter(SafeExceptionReporterFilter):
+    """
+    Ignores all the filtering done by its parent class.
+    """
+    
+    def get_post_parameters(self, request):
+        return request.POST
+            
+    def get_traceback_frame_variables(self, request, tb_frame):
+        return tb_frame.f_locals.items()
+
+
+@sensitive_variables()
+@sensitive_post_parameters()
+def custom_exception_reporter_filter_view(request):
+    # Do not just use plain strings for the variables' values in the code
+    # so that the tests don't return false positives when the function's source
+    # is displayed in the exception report.
+    cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd'])
+    sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e'])
+    request.exception_reporter_filter = UnsafeExceptionReporterFilter()
+    try:
+        raise Exception
+    except Exception:
+        exc_info = sys.exc_info()
+        send_log(request, exc_info)
+        return technical_500_response(request, *exc_info)
