diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index c2a9470ab1..d103900bdf 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -437,15 +437,25 @@ class CsrfViewMiddleware(MiddlewareMixin): return self._accept(request) def process_response(self, request, response): - if not getattr(request, 'csrf_cookie_needs_reset', False): - if getattr(response, 'csrf_cookie_set', False): - return response + # Send the CSRF cookie whenever the cookie is being used (even if the + # client already has it) in order to renew the expiry timer, but only + # if it hasn't already been sent during this request-response cycle. + # Also, send the cookie no matter what if a reset was requested. + if ( + getattr(request, 'csrf_cookie_needs_reset', False) or ( + request.META.get('CSRF_COOKIE_USED') and + not getattr(response, 'csrf_cookie_set', False) + ) + ): + self._set_token(request, response) + # Update state to prevent _set_token() from being unnecessarily + # called again in process_response() by other instances of + # CsrfViewMiddleware. This can happen e.g. when both a decorator + # and middleware are used. However, the csrf_cookie_needs_reset + # attribute is still respected in subsequent calls e.g. in case + # rotate_token() is called in process_response() later by custom + # middleware but before those subsequent calls. + response.csrf_cookie_set = True + request.csrf_cookie_needs_reset = False - if not request.META.get("CSRF_COOKIE_USED", False): - return response - - # Set the CSRF cookie even if it's already set, so we renew - # the expiry timer. - self._set_token(request, response) - response.csrf_cookie_set = True return response diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index 691a5c1e2b..216625067c 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -14,8 +14,9 @@ from django.test import SimpleTestCase, override_settings from django.views.decorators.csrf import csrf_exempt, requires_csrf_token from .views import ( - ensure_csrf_cookie_view, non_token_view_using_request_processor, - post_form_view, sandwiched_rotate_token_view, token_view, + ensure_csrf_cookie_view, ensured_and_protected_view, + non_token_view_using_request_processor, post_form_view, protected_view, + sandwiched_rotate_token_view, token_view, ) # This is a test (unmasked) CSRF cookie / secret. @@ -1065,6 +1066,32 @@ class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase): resp = mw.process_view(req, token_view, (), {}) self.assertIsNone(resp) + def test_cookie_reset_only_once(self): + """ + A CSRF cookie that needs to be reset is reset only once when the view + is decorated with both ensure_csrf_cookie and csrf_protect. + """ + # Pass an unmasked cookie to trigger a cookie reset. + req = self._get_POST_request_with_token(cookie=TEST_SECRET) + resp = ensured_and_protected_view(req) + self.assertContains(resp, 'OK') + csrf_cookie = self._read_csrf_cookie(req, resp) + actual_secret = _unmask_cipher_token(csrf_cookie) + self.assertEqual(actual_secret, TEST_SECRET) + # set_cookie() was called only once and with the expected secret. + self.assertCookiesSet(req, resp, [TEST_SECRET]) + + def test_invalid_cookie_replaced_on_GET(self): + """ + A CSRF cookie with the wrong format is replaced during a GET request. + """ + req = self._get_request(cookie='badvalue') + resp = protected_view(req) + self.assertContains(resp, 'OK') + csrf_cookie = self._read_csrf_cookie(req, resp) + self.assertTrue(csrf_cookie, msg='No CSRF cookie was sent.') + self.assertEqual(len(csrf_cookie), CSRF_TOKEN_LENGTH) + def test_bare_secret_accepted_and_replaced(self): """ The csrf token is reset from a bare secret. diff --git a/tests/csrf_tests/views.py b/tests/csrf_tests/views.py index fce9ae06b1..2dafbf28e2 100644 --- a/tests/csrf_tests/views.py +++ b/tests/csrf_tests/views.py @@ -33,6 +33,17 @@ class _CsrfCookieRotator(MiddlewareMixin): csrf_rotating_token = decorator_from_middleware(_CsrfCookieRotator) +@csrf_protect +def protected_view(request): + return HttpResponse('OK') + + +@csrf_protect +@ensure_csrf_cookie +def ensured_and_protected_view(request): + return TestingHttpResponse('OK') + + @csrf_protect @csrf_rotating_token @ensure_csrf_cookie