summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2021-07-04 02:29:52 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-07-23 06:56:53 +0200
commit311401d9a278339795a7ab3f1617c4da9077fdcc (patch)
treedb68623ac2a24cf1275f8bf0b7e46778055597ec
parent019424e44efe495bc5981eb9848c0bb398a6f068 (diff)
Refs #32902 -- Added CSRF test when rotate_token() is called between resetting the token and processing response.
-rw-r--r--tests/csrf_tests/tests.py60
-rw-r--r--tests/csrf_tests/views.py43
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):