Browse Source

Refs #16859 -- Allowed storing CSRF tokens in sessions.

Major thanks to Shai for helping to refactor the tests, and to
Shai, Tim, Florian, and others for extensive and helpful review.
Raphael Michel 8 years ago
parent
commit
ddf169cdac

+ 1 - 0
django/conf/global_settings.py

@@ -548,6 +548,7 @@ CSRF_COOKIE_SECURE = False
 CSRF_COOKIE_HTTPONLY = False
 CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
 CSRF_TRUSTED_ORIGINS = []
+CSRF_USE_SESSIONS = False
 
 ############
 # MESSAGES #

+ 54 - 25
django/middleware/csrf.py

@@ -11,6 +11,7 @@ import re
 import string
 
 from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
 from django.urls import get_callable
 from django.utils.cache import patch_vary_headers
 from django.utils.crypto import constant_time_compare, get_random_string
@@ -32,6 +33,7 @@ REASON_INSECURE_REFERER = "Referer checking failed - Referer is insecure while h
 CSRF_SECRET_LENGTH = 32
 CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
 CSRF_ALLOWED_CHARS = string.ascii_letters + string.digits
+CSRF_SESSION_KEY = '_csrftoken'
 
 
 def _get_failure_view():
@@ -160,20 +162,51 @@ class CsrfViewMiddleware(MiddlewareMixin):
         )
         return _get_failure_view()(request, reason=reason)
 
-    def process_view(self, request, callback, callback_args, callback_kwargs):
-        if getattr(request, 'csrf_processing_done', False):
-            return None
-
-        try:
-            cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
-        except KeyError:
-            csrf_token = None
+    def _get_token(self, request):
+        if settings.CSRF_USE_SESSIONS:
+            try:
+                return request.session.get(CSRF_SESSION_KEY)
+            except AttributeError:
+                raise ImproperlyConfigured(
+                    'CSRF_USE_SESSIONS is enabled, but request.session is not '
+                    'set. SessionMiddleware must appear before CsrfViewMiddleware '
+                    'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
+                )
         else:
+            try:
+                cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
+            except KeyError:
+                return None
+
             csrf_token = _sanitize_token(cookie_token)
             if csrf_token != cookie_token:
                 # Cookie token needed to be replaced;
                 # the cookie needs to be reset.
                 request.csrf_cookie_needs_reset = True
+            return csrf_token
+
+    def _set_token(self, request, response):
+        if settings.CSRF_USE_SESSIONS:
+            request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
+        else:
+            response.set_cookie(
+                settings.CSRF_COOKIE_NAME,
+                request.META['CSRF_COOKIE'],
+                max_age=settings.CSRF_COOKIE_AGE,
+                domain=settings.CSRF_COOKIE_DOMAIN,
+                path=settings.CSRF_COOKIE_PATH,
+                secure=settings.CSRF_COOKIE_SECURE,
+                httponly=settings.CSRF_COOKIE_HTTPONLY,
+            )
+            # Set the Vary header since content varies with the CSRF cookie.
+            patch_vary_headers(response, ('Cookie',))
+
+    def process_view(self, request, callback, callback_args, callback_kwargs):
+        if getattr(request, 'csrf_processing_done', False):
+            return None
+
+        csrf_token = self._get_token(request)
+        if csrf_token is not None:
             # Use same token next time.
             request.META['CSRF_COOKIE'] = csrf_token
 
@@ -226,16 +259,21 @@ class CsrfViewMiddleware(MiddlewareMixin):
                 if referer.scheme != 'https':
                     return self._reject(request, REASON_INSECURE_REFERER)
 
-                # If there isn't a CSRF_COOKIE_DOMAIN, assume we need an exact
-                # match on host:port. If not, obey the cookie rules.
-                if settings.CSRF_COOKIE_DOMAIN is None:
-                    # request.get_host() includes the port.
-                    good_referer = request.get_host()
-                else:
-                    good_referer = settings.CSRF_COOKIE_DOMAIN
+                # If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
+                # match on host:port. If not, obey the cookie rules (or those
+                # for the session cookie, if CSRF_USE_SESSIONS).
+                good_referer = (
+                    settings.SESSION_COOKIE_DOMAIN
+                    if settings.CSRF_USE_SESSIONS
+                    else settings.CSRF_COOKIE_DOMAIN
+                )
+                if good_referer is not None:
                     server_port = request.get_port()
                     if server_port not in ('443', '80'):
                         good_referer = '%s:%s' % (good_referer, server_port)
+                else:
+                    # request.get_host() includes the port.
+                    good_referer = request.get_host()
 
                 # Here we generate a list of all acceptable HTTP referers,
                 # including the current host since that has been validated
@@ -287,15 +325,6 @@ class CsrfViewMiddleware(MiddlewareMixin):
 
         # Set the CSRF cookie even if it's already set, so we renew
         # the expiry timer.
-        response.set_cookie(settings.CSRF_COOKIE_NAME,
-                            request.META["CSRF_COOKIE"],
-                            max_age=settings.CSRF_COOKIE_AGE,
-                            domain=settings.CSRF_COOKIE_DOMAIN,
-                            path=settings.CSRF_COOKIE_PATH,
-                            secure=settings.CSRF_COOKIE_SECURE,
-                            httponly=settings.CSRF_COOKIE_HTTPONLY
-                            )
-        # Content varies with the CSRF cookie, so set the Vary header.
-        patch_vary_headers(response, ('Cookie',))
+        self._set_token(request, response)
         response.csrf_cookie_set = True
         return response

+ 26 - 3
docs/ref/csrf.txt

@@ -64,9 +64,14 @@ XMLHttpRequest, set a custom ``X-CSRFToken`` header to the value of the CSRF
 token. This is often easier, because many JavaScript frameworks provide hooks
 that allow headers to be set on every request.
 
-As a first step, you must get the CSRF token itself. The recommended source for
-the token is the ``csrftoken`` cookie, which will be set if you've enabled CSRF
-protection for your views as outlined above.
+First, you must get the CSRF token. How to do that depends on whether or not
+the :setting:`CSRF_USE_SESSIONS` setting is enabled.
+
+Acquiring the token if :setting:`CSRF_USE_SESSIONS` is ``False``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The recommended source for the token is the ``csrftoken`` cookie, which will be
+set if you've enabled CSRF protection for your views as outlined above.
 
 .. note::
 
@@ -121,6 +126,23 @@ The above code could be simplified by using the `JavaScript Cookie library
     Django provides a view decorator which forces setting of the cookie:
     :func:`~django.views.decorators.csrf.ensure_csrf_cookie`.
 
+Acquiring the token if :setting:`CSRF_USE_SESSIONS` is ``True``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you activate :setting:`CSRF_USE_SESSIONS`, you must include the CSRF token
+in your HTML and read the token from the DOM with JavaScript:
+
+.. code-block:: html+django
+
+    {% csrf_token %}
+    <script type="text/javascript">
+    // using jQuery
+    var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
+    </script>
+
+Setting the token on the AJAX request
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 Finally, you'll have to actually set the header on your AJAX request, while
 protecting the CSRF token from being sent to other domains using
 `settings.crossDomain <https://api.jquery.com/jQuery.ajax>`_ in jQuery 1.5.1 and
@@ -493,6 +515,7 @@ A number of settings can be used to control Django's CSRF behavior:
 * :setting:`CSRF_FAILURE_VIEW`
 * :setting:`CSRF_HEADER_NAME`
 * :setting:`CSRF_TRUSTED_ORIGINS`
+* :setting:`CSRF_USE_SESSIONS`
 
 Frequently Asked Questions
 ==========================

+ 3 - 0
docs/ref/middleware.txt

@@ -512,6 +512,9 @@ Here are some hints about the ordering of various Django middleware classes:
    Before any view middleware that assumes that CSRF attacks have been dealt
    with.
 
+   It must come after ``SessionMiddleware`` if you're using
+   :setting:`CSRF_USE_SESSIONS`.
+
 #. :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`
 
    After ``SessionMiddleware``: uses session storage.

+ 17 - 0
docs/ref/settings.txt

@@ -377,6 +377,22 @@ Whether to use a secure cookie for the CSRF cookie. If this is set to ``True``,
 the cookie will be marked as "secure," which means browsers may ensure that the
 cookie is only sent with an HTTPS connection.
 
+.. setting:: CSRF_USE_SESSIONS
+
+``CSRF_USE_SESSIONS``
+---------------------
+
+.. versionadded:: 1.11
+
+Default: ``False``
+
+Whether to store the CSRF token in the user's session instead of in a cookie.
+It requires the use of :mod:`django.contrib.sessions`.
+
+Storing the CSRF token in a cookie (Django's default) is safe, but storing it
+in the session is common practice in other web frameworks and therefore
+sometimes demanded by security auditors.
+
 .. setting:: CSRF_FAILURE_VIEW
 
 ``CSRF_FAILURE_VIEW``
@@ -3407,6 +3423,7 @@ Security
   * :setting:`CSRF_FAILURE_VIEW`
   * :setting:`CSRF_HEADER_NAME`
   * :setting:`CSRF_TRUSTED_ORIGINS`
+  * :setting:`CSRF_USE_SESSIONS`
 
 * :setting:`SECRET_KEY`
 * :setting:`X_FRAME_OPTIONS`

+ 2 - 1
docs/releases/1.11.txt

@@ -231,7 +231,8 @@ Cache
 CSRF
 ~~~~
 
-* ...
+* Added the :setting:`CSRF_USE_SESSIONS` setting to allow storing the CSRF
+  token in the user's session rather than in a cookie.
 
 Database backends
 ~~~~~~~~~~~~~~~~~

+ 304 - 189
tests/csrf_tests/tests.py

@@ -6,10 +6,12 @@ import re
 import warnings
 
 from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
 from django.http import HttpRequest, HttpResponse
 from django.middleware.csrf import (
-    CSRF_TOKEN_LENGTH, REASON_BAD_TOKEN, REASON_NO_CSRF_COOKIE,
-    CsrfViewMiddleware, _compare_salted_tokens as equivalent_tokens, get_token,
+    CSRF_SESSION_KEY, CSRF_TOKEN_LENGTH, REASON_BAD_TOKEN,
+    REASON_NO_CSRF_COOKIE, CsrfViewMiddleware,
+    _compare_salted_tokens as equivalent_tokens, get_token,
 )
 from django.template import RequestContext, Template
 from django.template.context_processors import csrf
@@ -58,20 +60,27 @@ class TestingHttpRequest(HttpRequest):
     A version of HttpRequest that allows us to change some things
     more easily
     """
+    def __init__(self):
+        super(TestingHttpRequest, self).__init__()
+        # A real session backend isn't needed.
+        self.session = {}
+
     def is_secure(self):
         return getattr(self, '_is_secure_override', False)
 
 
-class CsrfViewMiddlewareTest(SimpleTestCase):
+class CsrfViewMiddlewareTestMixin(object):
+    """
+    Shared methods and tests for session-based and cookie-based tokens.
+    """
+
     _csrf_id = _csrf_id_cookie = '1bcdefghij2bcdefghij3bcdefghij4bcdefghij5bcdefghij6bcdefghijABCD'
 
     def _get_GET_no_csrf_cookie_request(self):
         return TestingHttpRequest()
 
     def _get_GET_csrf_cookie_request(self):
-        req = TestingHttpRequest()
-        req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie
-        return req
+        raise NotImplementedError('This method must be implemented by a subclass.')
 
     def _get_POST_csrf_cookie_request(self):
         req = self._get_GET_csrf_cookie_request()
@@ -88,16 +97,6 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
         req.POST['csrfmiddlewaretoken'] = self._csrf_id
         return req
 
-    def _get_POST_bare_secret_csrf_cookie_request(self):
-        req = self._get_POST_no_csrf_cookie_request()
-        req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie[:32]
-        return req
-
-    def _get_POST_bare_secret_csrf_cookie_request_with_token(self):
-        req = self._get_POST_bare_secret_csrf_cookie_request()
-        req.POST['csrfmiddlewaretoken'] = self._csrf_id_cookie[:32]
-        return req
-
     def _check_token_present(self, response, csrf_id=None):
         text = text_type(response.content, response.charset)
         match = re.search("name='csrfmiddlewaretoken' value='(.*?)'", text)
@@ -107,77 +106,6 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
             "Could not find csrfmiddlewaretoken to match %s" % csrf_token
         )
 
-    def test_process_view_token_too_long(self):
-        """
-        If the token is longer than expected, it is ignored and a new token is
-        created.
-        """
-        req = self._get_GET_no_csrf_cookie_request()
-        req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 100000
-        CsrfViewMiddleware().process_view(req, token_view, (), {})
-        resp = token_view(req)
-        resp2 = CsrfViewMiddleware().process_response(req, resp)
-        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
-        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
-
-    def test_process_view_token_invalid_chars(self):
-        """
-        If the token contains non-alphanumeric characters, it is ignored and a
-        new token is created.
-        """
-        token = ('!@#' + self._csrf_id)[:CSRF_TOKEN_LENGTH]
-        req = self._get_GET_no_csrf_cookie_request()
-        req.COOKIES[settings.CSRF_COOKIE_NAME] = token
-        CsrfViewMiddleware().process_view(req, token_view, (), {})
-        resp = token_view(req)
-        resp2 = CsrfViewMiddleware().process_response(req, resp)
-        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
-        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
-        self.assertNotEqual(csrf_cookie.value, token)
-
-    def test_process_view_token_invalid_bytes(self):
-        """
-        If the token contains improperly encoded unicode, it is ignored and a
-        new token is created.
-        """
-        token = (b"<1>\xc2\xa1" + force_bytes(self._csrf_id, 'ascii'))[:CSRF_TOKEN_LENGTH]
-        req = self._get_GET_no_csrf_cookie_request()
-        req.COOKIES[settings.CSRF_COOKIE_NAME] = token
-        # We expect a UnicodeWarning here, because we used broken utf-8 on purpose
-        with warnings.catch_warnings():
-            warnings.filterwarnings("ignore", category=UnicodeWarning)
-            CsrfViewMiddleware().process_view(req, token_view, (), {})
-        resp = token_view(req)
-        resp2 = CsrfViewMiddleware().process_response(req, resp)
-        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
-        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
-        self.assertNotEqual(csrf_cookie.value, token)
-
-    def test_process_response_get_token_used(self):
-        """
-        When get_token is used, check that the cookie is created and headers
-        patched.
-        """
-        req = self._get_GET_no_csrf_cookie_request()
-
-        # Put tests for CSRF_COOKIE_* settings here
-        with self.settings(CSRF_COOKIE_NAME='myname',
-                           CSRF_COOKIE_DOMAIN='.example.com',
-                           CSRF_COOKIE_PATH='/test/',
-                           CSRF_COOKIE_SECURE=True,
-                           CSRF_COOKIE_HTTPONLY=True):
-            # token_view calls get_token() indirectly
-            CsrfViewMiddleware().process_view(req, token_view, (), {})
-            resp = token_view(req)
-            resp2 = CsrfViewMiddleware().process_response(req, resp)
-        csrf_cookie = resp2.cookies.get('myname', False)
-        self.assertIsNot(csrf_cookie, False)
-        self.assertEqual(csrf_cookie['domain'], '.example.com')
-        self.assertIs(csrf_cookie['secure'], True)
-        self.assertIs(csrf_cookie['httponly'], True)
-        self.assertEqual(csrf_cookie['path'], '/test/')
-        self.assertIn('Cookie', resp2.get('Vary', ''))
-
     def test_process_response_get_token_not_used(self):
         """
         If get_token() is not called, the view middleware does not
@@ -371,20 +299,6 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
                 "CSRF cookie was changed on an accepted request"
             )
 
-    def test_bare_secret_accepted_and_replaced(self):
-        """
-        The csrf token is reset from a bare secret.
-        """
-        req = self._get_POST_bare_secret_csrf_cookie_request_with_token()
-        req2 = CsrfViewMiddleware().process_view(req, token_view, (), {})
-        self.assertIsNone(req2)
-        resp = token_view(req)
-        resp = CsrfViewMiddleware().process_response(req, resp)
-        self.assertIn(settings.CSRF_COOKIE_NAME, resp.cookies, "Cookie was not reset from bare secret")
-        csrf_cookie = resp.cookies[settings.CSRF_COOKIE_NAME]
-        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
-        self._check_token_present(resp, csrf_id=csrf_cookie.value)
-
     @override_settings(DEBUG=True, ALLOWED_HOSTS=['www.example.com'])
     def test_https_bad_referer(self):
         """
@@ -465,11 +379,7 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
         req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
         self.assertIsNone(req2)
 
-    @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_COOKIE_DOMAIN='.example.com', USE_X_FORWARDED_PORT=True)
-    def test_https_good_referer_behind_proxy(self):
-        """
-        A POST HTTPS request is accepted when USE_X_FORWARDED_PORT=True.
-        """
+    def _test_https_good_referer_behind_proxy(self):
         req = self._get_POST_request_with_token()
         req._is_secure_override = True
         req.META.update({
@@ -508,12 +418,7 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
         response = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
         self.assertIsNone(response)
 
-    @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_COOKIE_DOMAIN='.example.com')
-    def test_https_good_referer_matches_cookie_domain(self):
-        """
-        A POST HTTPS request with a good referer should be accepted from a
-        subdomain that's allowed by CSRF_COOKIE_DOMAIN.
-        """
+    def _test_https_good_referer_matches_cookie_domain(self):
         req = self._get_POST_request_with_token()
         req._is_secure_override = True
         req.META['HTTP_REFERER'] = 'https://foo.example.com/'
@@ -521,12 +426,7 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
         response = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
         self.assertIsNone(response)
 
-    @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_COOKIE_DOMAIN='.example.com')
-    def test_https_good_referer_matches_cookie_domain_with_different_port(self):
-        """
-        A POST HTTPS request with a good referer should be accepted from a
-        subdomain that's allowed by CSRF_COOKIE_DOMAIN and a non-443 port.
-        """
+    def _test_https_good_referer_matches_cookie_domain_with_different_port(self):
         req = self._get_POST_request_with_token()
         req._is_secure_override = True
         req.META['HTTP_HOST'] = 'www.example.com'
@@ -535,21 +435,98 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
         response = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
         self.assertIsNone(response)
 
-    @override_settings(CSRF_COOKIE_DOMAIN='.example.com', DEBUG=True)
-    def test_https_reject_insecure_referer(self):
+    def test_ensures_csrf_cookie_no_logging(self):
         """
-        A POST HTTPS request from an insecure referer should be rejected.
+        ensure_csrf_cookie() doesn't log warnings (#19436).
         """
-        req = self._get_POST_request_with_token()
-        req._is_secure_override = True
-        req.META['HTTP_REFERER'] = 'http://example.com/'
-        req.META['SERVER_PORT'] = '443'
-        response = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
-        self.assertContains(
-            response,
-            'Referer checking failed - Referer is insecure while host is secure.',
-            status_code=403,
-        )
+        @ensure_csrf_cookie
+        def view(request):
+            # Doesn't insert a token or anything
+            return HttpResponse(content="")
+
+        class TestHandler(logging.Handler):
+            def emit(self, record):
+                raise Exception("This shouldn't have happened!")
+
+        logger = logging.getLogger('django.request')
+        test_handler = TestHandler()
+        old_log_level = logger.level
+        try:
+            logger.addHandler(test_handler)
+            logger.setLevel(logging.WARNING)
+
+            req = self._get_GET_no_csrf_cookie_request()
+            view(req)
+        finally:
+            logger.removeHandler(test_handler)
+            logger.setLevel(old_log_level)
+
+    def test_post_data_read_failure(self):
+        """
+        #20128 -- IOErrors during POST data reading should be caught and
+        treated as if the POST data wasn't there.
+        """
+        class CsrfPostRequest(HttpRequest):
+            """
+            HttpRequest that can raise an IOError when accessing POST data
+            """
+            def __init__(self, token, raise_error):
+                super(CsrfPostRequest, self).__init__()
+                self.method = 'POST'
+
+                self.raise_error = False
+                self.COOKIES[settings.CSRF_COOKIE_NAME] = token
+
+                # Handle both cases here to prevent duplicate code in the
+                # session tests.
+                self.session = {}
+                self.session[CSRF_SESSION_KEY] = token
+
+                self.POST['csrfmiddlewaretoken'] = token
+                self.raise_error = raise_error
+
+            def _load_post_and_files(self):
+                raise IOError('error reading input data')
+
+            def _get_post(self):
+                if self.raise_error:
+                    self._load_post_and_files()
+                return self._post
+
+            def _set_post(self, post):
+                self._post = post
+
+            POST = property(_get_post, _set_post)
+
+        token = ('ABC' + self._csrf_id)[:CSRF_TOKEN_LENGTH]
+
+        req = CsrfPostRequest(token, raise_error=False)
+        resp = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertIsNone(resp)
+
+        req = CsrfPostRequest(token, raise_error=True)
+        with patch_logger('django.security.csrf', 'warning') as logger_calls:
+            resp = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+            self.assertEqual(resp.status_code, 403)
+            self.assertEqual(logger_calls[0], 'Forbidden (%s): ' % REASON_BAD_TOKEN)
+
+
+class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase):
+
+    def _get_GET_csrf_cookie_request(self):
+        req = TestingHttpRequest()
+        req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie
+        return req
+
+    def _get_POST_bare_secret_csrf_cookie_request(self):
+        req = self._get_POST_no_csrf_cookie_request()
+        req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id_cookie[:32]
+        return req
+
+    def _get_POST_bare_secret_csrf_cookie_request_with_token(self):
+        req = self._get_POST_bare_secret_csrf_cookie_request()
+        req.POST['csrfmiddlewaretoken'] = self._csrf_id_cookie[:32]
+        return req
 
     def test_ensures_csrf_cookie_no_middleware(self):
         """
@@ -582,32 +559,6 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
         self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False))
         self.assertIn('Cookie', resp2.get('Vary', ''))
 
-    def test_ensures_csrf_cookie_no_logging(self):
-        """
-        ensure_csrf_cookie() doesn't log warnings (#19436).
-        """
-        @ensure_csrf_cookie
-        def view(request):
-            # Doesn't insert a token or anything
-            return HttpResponse(content="")
-
-        class TestHandler(logging.Handler):
-            def emit(self, record):
-                raise Exception("This shouldn't have happened!")
-
-        logger = logging.getLogger('django.request')
-        test_handler = TestHandler()
-        old_log_level = logger.level
-        try:
-            logger.addHandler(test_handler)
-            logger.setLevel(logging.WARNING)
-
-            req = self._get_GET_no_csrf_cookie_request()
-            view(req)
-        finally:
-            logger.removeHandler(test_handler)
-            logger.setLevel(old_log_level)
-
     def test_csrf_cookie_age(self):
         """
         CSRF cookie age can be set using settings.CSRF_COOKIE_AGE.
@@ -651,45 +602,209 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
             max_age = resp2.cookies.get('csrfcookie').get('max-age')
             self.assertEqual(max_age, '')
 
-    def test_post_data_read_failure(self):
+    def test_process_view_token_too_long(self):
         """
-        #20128 -- IOErrors during POST data reading should be caught and
-        treated as if the POST data wasn't there.
+        If the token is longer than expected, it is ignored and a new token is
+        created.
         """
-        class CsrfPostRequest(HttpRequest):
-            """
-            HttpRequest that can raise an IOError when accessing POST data
-            """
-            def __init__(self, token, raise_error):
-                super(CsrfPostRequest, self).__init__()
-                self.method = 'POST'
+        req = self._get_GET_no_csrf_cookie_request()
+        req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 100000
+        CsrfViewMiddleware().process_view(req, token_view, (), {})
+        resp = token_view(req)
+        resp2 = CsrfViewMiddleware().process_response(req, resp)
+        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
+        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
 
-                self.raise_error = False
-                self.COOKIES[settings.CSRF_COOKIE_NAME] = token
-                self.POST['csrfmiddlewaretoken'] = token
-                self.raise_error = raise_error
+    def test_process_view_token_invalid_bytes(self):
+        """
+        If the token contains improperly encoded unicode, it is ignored and a
+        new token is created.
+        """
+        token = (b"<1>\xc2\xa1" + force_bytes(self._csrf_id, 'ascii'))[:CSRF_TOKEN_LENGTH]
+        req = self._get_GET_no_csrf_cookie_request()
+        req.COOKIES[settings.CSRF_COOKIE_NAME] = token
+        # We expect a UnicodeWarning here, because we used broken utf-8 on purpose
+        with warnings.catch_warnings():
+            warnings.filterwarnings("ignore", category=UnicodeWarning)
+            CsrfViewMiddleware().process_view(req, token_view, (), {})
+        resp = token_view(req)
+        resp2 = CsrfViewMiddleware().process_response(req, resp)
+        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
+        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
+        self.assertNotEqual(csrf_cookie.value, token)
 
-            def _load_post_and_files(self):
-                raise IOError('error reading input data')
+    def test_process_view_token_invalid_chars(self):
+        """
+        If the token contains non-alphanumeric characters, it is ignored and a
+        new token is created.
+        """
+        token = ('!@#' + self._csrf_id)[:CSRF_TOKEN_LENGTH]
+        req = self._get_GET_no_csrf_cookie_request()
+        req.COOKIES[settings.CSRF_COOKIE_NAME] = token
+        CsrfViewMiddleware().process_view(req, token_view, (), {})
+        resp = token_view(req)
+        resp2 = CsrfViewMiddleware().process_response(req, resp)
+        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
+        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
+        self.assertNotEqual(csrf_cookie.value, token)
 
-            def _get_post(self):
-                if self.raise_error:
-                    self._load_post_and_files()
-                return self._post
+    def test_bare_secret_accepted_and_replaced(self):
+        """
+        The csrf token is reset from a bare secret.
+        """
+        req = self._get_POST_bare_secret_csrf_cookie_request_with_token()
+        req2 = CsrfViewMiddleware().process_view(req, token_view, (), {})
+        self.assertIsNone(req2)
+        resp = token_view(req)
+        resp = CsrfViewMiddleware().process_response(req, resp)
+        self.assertIn(settings.CSRF_COOKIE_NAME, resp.cookies, "Cookie was not reset from bare secret")
+        csrf_cookie = resp.cookies[settings.CSRF_COOKIE_NAME]
+        self.assertEqual(len(csrf_cookie.value), CSRF_TOKEN_LENGTH)
+        self._check_token_present(resp, csrf_id=csrf_cookie.value)
 
-            def _set_post(self, post):
-                self._post = post
+    @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_COOKIE_DOMAIN='.example.com', USE_X_FORWARDED_PORT=True)
+    def test_https_good_referer_behind_proxy(self):
+        """
+        A POST HTTPS request is accepted when USE_X_FORWARDED_PORT=True.
+        """
+        self._test_https_good_referer_behind_proxy()
 
-            POST = property(_get_post, _set_post)
+    @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_COOKIE_DOMAIN='.example.com')
+    def test_https_good_referer_matches_cookie_domain(self):
+        """
+        A POST HTTPS request with a good referer should be accepted from a
+        subdomain that's allowed by CSRF_COOKIE_DOMAIN.
+        """
+        self._test_https_good_referer_matches_cookie_domain()
 
-        token = ('ABC' + self._csrf_id)[:CSRF_TOKEN_LENGTH]
+    @override_settings(ALLOWED_HOSTS=['www.example.com'], CSRF_COOKIE_DOMAIN='.example.com')
+    def test_https_good_referer_matches_cookie_domain_with_different_port(self):
+        """
+        A POST HTTPS request with a good referer should be accepted from a
+        subdomain that's allowed by CSRF_COOKIE_DOMAIN and a non-443 port.
+        """
+        self._test_https_good_referer_matches_cookie_domain_with_different_port()
 
-        req = CsrfPostRequest(token, raise_error=False)
-        resp = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
-        self.assertIsNone(resp)
+    @override_settings(CSRF_COOKIE_DOMAIN='.example.com', DEBUG=True)
+    def test_https_reject_insecure_referer(self):
+        """
+        A POST HTTPS request from an insecure referer should be rejected.
+        """
+        req = self._get_POST_request_with_token()
+        req._is_secure_override = True
+        req.META['HTTP_REFERER'] = 'http://example.com/'
+        req.META['SERVER_PORT'] = '443'
+        response = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertContains(
+            response,
+            'Referer checking failed - Referer is insecure while host is secure.',
+            status_code=403,
+        )
 
-        req = CsrfPostRequest(token, raise_error=True)
-        with patch_logger('django.security.csrf', 'warning') as logger_calls:
-            resp = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
-            self.assertEqual(resp.status_code, 403)
-            self.assertEqual(logger_calls[0], 'Forbidden (%s): ' % REASON_BAD_TOKEN)
+
+@override_settings(CSRF_USE_SESSIONS=True, CSRF_COOKIE_DOMAIN=None)
+class CsrfViewMiddlewareUseSessionsTests(CsrfViewMiddlewareTestMixin, SimpleTestCase):
+    """
+    CSRF tests with CSRF_USE_SESSIONS=True.
+    """
+
+    def _get_POST_bare_secret_csrf_cookie_request(self):
+        req = self._get_POST_no_csrf_cookie_request()
+        req.session[CSRF_SESSION_KEY] = self._csrf_id_cookie[:32]
+        return req
+
+    def _get_GET_csrf_cookie_request(self):
+        req = TestingHttpRequest()
+        req.session[CSRF_SESSION_KEY] = self._csrf_id_cookie
+        return req
+
+    def test_no_session_on_request(self):
+        msg = (
+            'CSRF_USE_SESSIONS is enabled, but request.session is not set. '
+            'SessionMiddleware must appear before CsrfViewMiddleware in MIDDLEWARE.'
+        )
+        with self.assertRaisesMessage(ImproperlyConfigured, msg):
+            CsrfViewMiddleware().process_view(HttpRequest(), None, (), {})
+
+    def test_process_response_get_token_used(self):
+        """The ensure_csrf_cookie() decorator works without middleware."""
+        @ensure_csrf_cookie
+        def view(request):
+            # Doesn't insert a token or anything
+            return HttpResponse(content="")
+
+        req = self._get_GET_no_csrf_cookie_request()
+        view(req)
+        self.assertTrue(req.session.get(CSRF_SESSION_KEY, False))
+
+    def test_ensures_csrf_cookie_with_middleware(self):
+        """
+        The ensure_csrf_cookie() decorator works with the CsrfViewMiddleware
+        enabled.
+        """
+        @ensure_csrf_cookie
+        def view(request):
+            # Doesn't insert a token or anything
+            return HttpResponse(content="")
+
+        req = self._get_GET_no_csrf_cookie_request()
+        CsrfViewMiddleware().process_view(req, view, (), {})
+        resp = view(req)
+        CsrfViewMiddleware().process_response(req, resp)
+        self.assertTrue(req.session.get(CSRF_SESSION_KEY, False))
+
+    def test_token_node_with_new_csrf_cookie(self):
+        """
+        CsrfTokenNode works when a CSRF cookie is created by the middleware
+        (when one was not already present).
+        """
+        req = self._get_GET_no_csrf_cookie_request()
+        CsrfViewMiddleware().process_view(req, token_view, (), {})
+        resp = token_view(req)
+        CsrfViewMiddleware().process_response(req, resp)
+        csrf_cookie = req.session[CSRF_SESSION_KEY]
+        self._check_token_present(resp, csrf_id=csrf_cookie)
+
+    @override_settings(
+        ALLOWED_HOSTS=['www.example.com'],
+        SESSION_COOKIE_DOMAIN='.example.com',
+        USE_X_FORWARDED_PORT=True,
+        DEBUG=True,
+    )
+    def test_https_good_referer_behind_proxy(self):
+        """
+        A POST HTTPS request is accepted when USE_X_FORWARDED_PORT=True.
+        """
+        self._test_https_good_referer_behind_proxy()
+
+    @override_settings(ALLOWED_HOSTS=['www.example.com'], SESSION_COOKIE_DOMAIN='.example.com')
+    def test_https_good_referer_matches_cookie_domain(self):
+        """
+        A POST HTTPS request with a good referer should be accepted from a
+        subdomain that's allowed by SESSION_COOKIE_DOMAIN.
+        """
+        self._test_https_good_referer_matches_cookie_domain()
+
+    @override_settings(ALLOWED_HOSTS=['www.example.com'], SESSION_COOKIE_DOMAIN='.example.com')
+    def test_https_good_referer_matches_cookie_domain_with_different_port(self):
+        """
+        A POST HTTPS request with a good referer should be accepted from a
+        subdomain that's allowed by SESSION_COOKIE_DOMAIN and a non-443 port.
+        """
+        self._test_https_good_referer_matches_cookie_domain_with_different_port()
+
+    @override_settings(SESSION_COOKIE_DOMAIN='.example.com', DEBUG=True)
+    def test_https_reject_insecure_referer(self):
+        """
+        A POST HTTPS request from an insecure referer should be rejected.
+        """
+        req = self._get_POST_request_with_token()
+        req._is_secure_override = True
+        req.META['HTTP_REFERER'] = 'http://example.com/'
+        req.META['SERVER_PORT'] = '443'
+        response = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+        self.assertContains(
+            response,
+            'Referer checking failed - Referer is insecure while host is secure.',
+            status_code=403,
+        )