Skip to content

Commit

Permalink
Refs django#16859 -- Allowed storing CSRF tokens in sessions.
Browse files Browse the repository at this point in the history
Major thanks to Shai for helping to refactor the tests, and to
Shai, Tim, Florian, and others for extensive and helpful review.
  • Loading branch information
raphaelm authored and timgraham committed Nov 30, 2016
1 parent f24eea3 commit ddf169c
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 218 deletions.
1 change: 1 addition & 0 deletions django/conf/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ def gettext_noop(s):
CSRF_COOKIE_HTTPONLY = False
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
CSRF_TRUSTED_ORIGINS = []
CSRF_USE_SESSIONS = False

############
# MESSAGES #
Expand Down
79 changes: 54 additions & 25 deletions django/middleware/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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
Expand All @@ -32,6 +33,7 @@
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():
Expand Down Expand Up @@ -160,20 +162,51 @@ def _reject(self, request, reason):
)
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

Expand Down Expand Up @@ -226,16 +259,21 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
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
Expand Down Expand Up @@ -287,15 +325,6 @@ def process_response(self, request, response):

# 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
29 changes: 26 additions & 3 deletions docs/ref/csrf.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
==========================
Expand Down
3 changes: 3 additions & 0 deletions docs/ref/middleware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions docs/ref/settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down Expand Up @@ -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`
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/1.11.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~~
Expand Down
Loading

0 comments on commit ddf169c

Please sign in to comment.