Kaydet (Commit) ddf169cd authored tarafından Raphael Michel's avatar Raphael Michel Kaydeden (comit) Tim Graham

Refs #16859 -- Allowed storing CSRF tokens in sessions.

Major thanks to Shai for helping to refactor the tests, and to
Shai, Tim, Florian, and others for extensive and helpful review.
üst f24eea3b
......@@ -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 #
......
......@@ -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
......@@ -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 %}
<script type="text/javascript">
// using jQuery
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
</script>
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 <https://api.jquery.com/jQuery.ajax>`_ 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
==========================
......
......@@ -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.
......
......@@ -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`
......
......@@ -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
~~~~~~~~~~~~~~~~~
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment