From 311401d9a278339795a7ab3f1617c4da9077fdcc Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 4 Jul 2021 02:29:52 -0400 Subject: [PATCH] Refs #32902 -- Added CSRF test when rotate_token() is called between resetting the token and processing response. --- tests/csrf_tests/tests.py | 60 ++++++++++++++++++++++++++++++++++++--- tests/csrf_tests/views.py | 43 ++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index d1f1f7e167..691a5c1e2b 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -15,7 +15,7 @@ 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, token_view, + post_form_view, sandwiched_rotate_token_view, token_view, ) # This is a test (unmasked) CSRF cookie / secret. @@ -69,14 +69,30 @@ class CsrfFunctionTests(SimpleTestCase): self.assertMaskedSecretCorrect(masked, secret) +class TestingSessionStore(SessionStore): + """ + A version of SessionStore that stores what cookie values are passed to + set_cookie() when CSRF_USE_SESSIONS=True. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # This is a list of the cookie values passed to set_cookie() over + # the course of the request-response. + self._cookies_set = [] + + def __setitem__(self, key, value): + super().__setitem__(key, value) + self._cookies_set.append(value) + + class TestingHttpRequest(HttpRequest): """ - A version of HttpRequest that allows us to change some things - more easily + A version of HttpRequest that lets one track and change some things more + easily. """ def __init__(self): super().__init__() - self.session = SessionStore() + self.session = TestingSessionStore() def is_secure(self): return getattr(self, '_is_secure_override', False) @@ -99,6 +115,21 @@ class CsrfViewMiddlewareTestMixin: """ raise NotImplementedError('This method must be implemented by a subclass.') + def _get_cookies_set(self, req, resp): + """ + Return a list of the cookie values passed to set_cookie() over the + course of the request-response. + """ + raise NotImplementedError('This method must be implemented by a subclass.') + + def assertCookiesSet(self, req, resp, expected_secrets): + """ + Assert that set_cookie() was called with the given sequence of secrets. + """ + cookies_set = self._get_cookies_set(req, resp) + secrets_set = [_unmask_cipher_token(cookie) for cookie in cookies_set] + self.assertEqual(secrets_set, expected_secrets) + def _get_request(self, method=None, cookie=None): if method is None: method = 'GET' @@ -332,6 +363,21 @@ class CsrfViewMiddlewareTestMixin: resp = mw.process_view(req, post_form_view, (), {}) self.assertIsNone(resp) + def test_rotate_token_triggers_second_reset(self): + """ + If rotate_token() is called after the token is reset in + CsrfViewMiddleware's process_response() and before another call to + the same process_response(), the cookie is reset a second time. + """ + req = self._get_POST_request_with_token() + resp = sandwiched_rotate_token_view(req) + self.assertContains(resp, 'OK') + csrf_cookie = self._read_csrf_cookie(req, resp) + actual_secret = _unmask_cipher_token(csrf_cookie) + # set_cookie() was called a second time with a different secret. + self.assertCookiesSet(req, resp, [TEST_SECRET, actual_secret]) + self.assertNotEqual(actual_secret, TEST_SECRET) + # Tests for the template tag method def test_token_node_no_csrf_cookie(self): """ @@ -875,6 +921,9 @@ class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase): csrf_cookie = resp.cookies[settings.CSRF_COOKIE_NAME] return csrf_cookie.value + def _get_cookies_set(self, req, resp): + return resp._cookies_set + def test_ensures_csrf_cookie_no_middleware(self): """ The ensure_csrf_cookie() decorator works without middleware. @@ -1089,6 +1138,9 @@ class CsrfViewMiddlewareUseSessionsTests(CsrfViewMiddlewareTestMixin, SimpleTest return False return req.session[CSRF_SESSION_KEY] + def _get_cookies_set(self, req, resp): + return req.session._cookies_set + def test_no_session_on_request(self): msg = ( 'CSRF_USE_SESSIONS is enabled, but request.session is not set. ' diff --git a/tests/csrf_tests/views.py b/tests/csrf_tests/views.py index 41bcb07ed6..fce9ae06b1 100644 --- a/tests/csrf_tests/views.py +++ b/tests/csrf_tests/views.py @@ -1,8 +1,47 @@ from django.http import HttpResponse -from django.middleware.csrf import get_token +from django.middleware.csrf import get_token, rotate_token from django.template import Context, RequestContext, Template from django.template.context_processors import csrf -from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.decorators import decorator_from_middleware +from django.utils.deprecation import MiddlewareMixin +from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie + + +class TestingHttpResponse(HttpResponse): + """ + A version of HttpResponse that stores what cookie values are passed to + set_cookie() when CSRF_USE_SESSIONS=False. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # This is a list of the cookie values passed to set_cookie() over + # the course of the request-response. + self._cookies_set = [] + + def set_cookie(self, key, value, **kwargs): + super().set_cookie(key, value, **kwargs) + self._cookies_set.append(value) + + +class _CsrfCookieRotator(MiddlewareMixin): + + def process_response(self, request, response): + rotate_token(request) + return response + + +csrf_rotating_token = decorator_from_middleware(_CsrfCookieRotator) + + +@csrf_protect +@csrf_rotating_token +@ensure_csrf_cookie +def sandwiched_rotate_token_view(request): + """ + This is a view that calls rotate_token() in process_response() between two + calls to CsrfViewMiddleware.process_response(). + """ + return TestingHttpResponse('OK') def post_form_view(request):