diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index b123c1136b..206d66f15d 100644
--- a/django/conf/global_settings.py
+++ b/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 #
diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py
index 3f9a649c27..d7359e4912 100644
--- a/django/middleware/csrf.py
+++ b/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
diff --git a/docs/ref/csrf.txt b/docs/ref/csrf.txt
index 2c1b8abbf5..3d1ecc1237 100644
--- a/docs/ref/csrf.txt
+++ b/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 %}
+
+
+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 `_ 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
==========================
diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt
index 94778d8a3e..dd0bae3bb3 100644
--- a/docs/ref/middleware.txt
+++ b/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.
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 5f669115ab..87dbc89584 100644
--- a/docs/ref/settings.txt
+++ b/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`
diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt
index fc4a5cc3f5..acc2fe10e2 100644
--- a/docs/releases/1.11.txt
+++ b/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
~~~~~~~~~~~~~~~~~
diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py
index cd7ea8fbc6..6369de9f99 100644
--- a/tests/csrf_tests/tests.py
+++ b/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,
+ )