Refs #32902 -- Added CSRF test when rotate_token() is called between resetting the token and processing response.

This commit is contained in:
Chris Jerdonek 2021-07-04 02:29:52 -04:00 committed by Mariusz Felisiak
parent 019424e44e
commit 311401d9a2
2 changed files with 97 additions and 6 deletions

View File

@ -15,7 +15,7 @@ from django.views.decorators.csrf import csrf_exempt, requires_csrf_token
from .views import ( from .views import (
ensure_csrf_cookie_view, non_token_view_using_request_processor, 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. # This is a test (unmasked) CSRF cookie / secret.
@ -69,14 +69,30 @@ class CsrfFunctionTests(SimpleTestCase):
self.assertMaskedSecretCorrect(masked, secret) 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): class TestingHttpRequest(HttpRequest):
""" """
A version of HttpRequest that allows us to change some things A version of HttpRequest that lets one track and change some things more
more easily easily.
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.session = SessionStore() self.session = TestingSessionStore()
def is_secure(self): def is_secure(self):
return getattr(self, '_is_secure_override', False) return getattr(self, '_is_secure_override', False)
@ -99,6 +115,21 @@ class CsrfViewMiddlewareTestMixin:
""" """
raise NotImplementedError('This method must be implemented by a subclass.') 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): def _get_request(self, method=None, cookie=None):
if method is None: if method is None:
method = 'GET' method = 'GET'
@ -332,6 +363,21 @@ class CsrfViewMiddlewareTestMixin:
resp = mw.process_view(req, post_form_view, (), {}) resp = mw.process_view(req, post_form_view, (), {})
self.assertIsNone(resp) 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 # Tests for the template tag method
def test_token_node_no_csrf_cookie(self): def test_token_node_no_csrf_cookie(self):
""" """
@ -875,6 +921,9 @@ class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase):
csrf_cookie = resp.cookies[settings.CSRF_COOKIE_NAME] csrf_cookie = resp.cookies[settings.CSRF_COOKIE_NAME]
return csrf_cookie.value return csrf_cookie.value
def _get_cookies_set(self, req, resp):
return resp._cookies_set
def test_ensures_csrf_cookie_no_middleware(self): def test_ensures_csrf_cookie_no_middleware(self):
""" """
The ensure_csrf_cookie() decorator works without middleware. The ensure_csrf_cookie() decorator works without middleware.
@ -1089,6 +1138,9 @@ class CsrfViewMiddlewareUseSessionsTests(CsrfViewMiddlewareTestMixin, SimpleTest
return False return False
return req.session[CSRF_SESSION_KEY] 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): def test_no_session_on_request(self):
msg = ( msg = (
'CSRF_USE_SESSIONS is enabled, but request.session is not set. ' 'CSRF_USE_SESSIONS is enabled, but request.session is not set. '

View File

@ -1,8 +1,47 @@
from django.http import HttpResponse 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 import Context, RequestContext, Template
from django.template.context_processors import csrf 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): def post_form_view(request):