Fixed #20869 -- made CSRF tokens change every request by salt-encrypting them
Note that the cookie is not changed every request, just the token retrieved by the `get_token()` method (used also by the `{% csrf_token %}` tag). While at it, made token validation strict: Where, before, any length was accepted and non-ASCII chars were ignored, we now treat anything other than `[A-Za-z0-9]{64}` as invalid (except for 32-char tokens, which, for backwards-compatibility, are accepted and replaced by 64-char ones). Thanks Trac user patrys for reporting, github user adambrenecki for initial patch, Tim Graham for help, and Curtis Maloney, Collin Anderson, Florian Apolloner, Markus Holtermann & Jon Dufresne for reviews.
This commit is contained in:
parent
6d9c5d46e6
commit
5112e65ef2
|
@ -8,6 +8,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import get_callable
|
from django.urls import get_callable
|
||||||
|
@ -16,8 +17,10 @@ from django.utils.crypto import constant_time_compare, get_random_string
|
||||||
from django.utils.deprecation import MiddlewareMixin
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.http import is_same_domain
|
from django.utils.http import is_same_domain
|
||||||
|
from django.utils.six.moves import zip
|
||||||
from django.utils.six.moves.urllib.parse import urlparse
|
from django.utils.six.moves.urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('django.request')
|
logger = logging.getLogger('django.request')
|
||||||
|
|
||||||
REASON_NO_REFERER = "Referer checking failed - no Referer."
|
REASON_NO_REFERER = "Referer checking failed - no Referer."
|
||||||
|
@ -27,7 +30,9 @@ REASON_BAD_TOKEN = "CSRF token missing or incorrect."
|
||||||
REASON_MALFORMED_REFERER = "Referer checking failed - Referer is malformed."
|
REASON_MALFORMED_REFERER = "Referer checking failed - Referer is malformed."
|
||||||
REASON_INSECURE_REFERER = "Referer checking failed - Referer is insecure while host is secure."
|
REASON_INSECURE_REFERER = "Referer checking failed - Referer is insecure while host is secure."
|
||||||
|
|
||||||
CSRF_KEY_LENGTH = 32
|
CSRF_SECRET_LENGTH = 32
|
||||||
|
CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
|
||||||
|
CSRF_ALLOWED_CHARS = string.ascii_letters + string.digits
|
||||||
|
|
||||||
|
|
||||||
def _get_failure_view():
|
def _get_failure_view():
|
||||||
|
@ -37,8 +42,38 @@ def _get_failure_view():
|
||||||
return get_callable(settings.CSRF_FAILURE_VIEW)
|
return get_callable(settings.CSRF_FAILURE_VIEW)
|
||||||
|
|
||||||
|
|
||||||
def _get_new_csrf_key():
|
def _get_new_csrf_string():
|
||||||
return get_random_string(CSRF_KEY_LENGTH)
|
return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)
|
||||||
|
|
||||||
|
|
||||||
|
def _salt_cipher_secret(secret):
|
||||||
|
"""
|
||||||
|
Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a
|
||||||
|
token by adding a salt and using it to encrypt the secret.
|
||||||
|
"""
|
||||||
|
salt = _get_new_csrf_string()
|
||||||
|
chars = CSRF_ALLOWED_CHARS
|
||||||
|
pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
|
||||||
|
cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
|
||||||
|
return salt + cipher
|
||||||
|
|
||||||
|
|
||||||
|
def _unsalt_cipher_token(token):
|
||||||
|
"""
|
||||||
|
Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length
|
||||||
|
CSRF_TOKEN_LENGTH, and that its first half is a salt), use it to decrypt
|
||||||
|
the second half to produce the original secret.
|
||||||
|
"""
|
||||||
|
salt = token[:CSRF_SECRET_LENGTH]
|
||||||
|
token = token[CSRF_SECRET_LENGTH:]
|
||||||
|
chars = CSRF_ALLOWED_CHARS
|
||||||
|
pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt))
|
||||||
|
secret = ''.join(chars[x - y] for x, y in pairs) # Note negative values are ok
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
def _get_new_csrf_token():
|
||||||
|
return _salt_cipher_secret(_get_new_csrf_string())
|
||||||
|
|
||||||
|
|
||||||
def get_token(request):
|
def get_token(request):
|
||||||
|
@ -52,9 +87,12 @@ def get_token(request):
|
||||||
function lazily, as is done by the csrf context processor.
|
function lazily, as is done by the csrf context processor.
|
||||||
"""
|
"""
|
||||||
if "CSRF_COOKIE" not in request.META:
|
if "CSRF_COOKIE" not in request.META:
|
||||||
request.META["CSRF_COOKIE"] = _get_new_csrf_key()
|
csrf_secret = _get_new_csrf_string()
|
||||||
|
request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
|
||||||
|
else:
|
||||||
|
csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
|
||||||
request.META["CSRF_COOKIE_USED"] = True
|
request.META["CSRF_COOKIE_USED"] = True
|
||||||
return request.META["CSRF_COOKIE"]
|
return _salt_cipher_secret(csrf_secret)
|
||||||
|
|
||||||
|
|
||||||
def rotate_token(request):
|
def rotate_token(request):
|
||||||
|
@ -64,19 +102,35 @@ def rotate_token(request):
|
||||||
"""
|
"""
|
||||||
request.META.update({
|
request.META.update({
|
||||||
"CSRF_COOKIE_USED": True,
|
"CSRF_COOKIE_USED": True,
|
||||||
"CSRF_COOKIE": _get_new_csrf_key(),
|
"CSRF_COOKIE": _get_new_csrf_token(),
|
||||||
})
|
})
|
||||||
|
request.csrf_cookie_needs_reset = True
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_token(token):
|
def _sanitize_token(token):
|
||||||
# Allow only alphanum
|
# Allow only ASCII alphanumerics
|
||||||
if len(token) > CSRF_KEY_LENGTH:
|
if re.search('[^a-zA-Z0-9]', force_text(token)):
|
||||||
return _get_new_csrf_key()
|
return _get_new_csrf_token()
|
||||||
token = re.sub('[^a-zA-Z0-9]+', '', force_text(token))
|
elif len(token) == CSRF_TOKEN_LENGTH:
|
||||||
if token == "":
|
return token
|
||||||
# In case the cookie has been truncated to nothing at some point.
|
elif len(token) == CSRF_SECRET_LENGTH:
|
||||||
return _get_new_csrf_key()
|
# Older Django versions set cookies to values of CSRF_SECRET_LENGTH
|
||||||
return token
|
# alphanumeric characters. For backwards compatibility, accept
|
||||||
|
# such values as unsalted secrets.
|
||||||
|
# It's easier to salt here and be consistent later, rather than add
|
||||||
|
# different code paths in the checks, although that might be a tad more
|
||||||
|
# efficient.
|
||||||
|
return _salt_cipher_secret(token)
|
||||||
|
return _get_new_csrf_token()
|
||||||
|
|
||||||
|
|
||||||
|
def _compare_salted_tokens(request_csrf_token, csrf_token):
|
||||||
|
# Assume both arguments are sanitized -- that is, strings of
|
||||||
|
# length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
|
||||||
|
return constant_time_compare(
|
||||||
|
_unsalt_cipher_token(request_csrf_token),
|
||||||
|
_unsalt_cipher_token(csrf_token),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CsrfViewMiddleware(MiddlewareMixin):
|
class CsrfViewMiddleware(MiddlewareMixin):
|
||||||
|
@ -112,12 +166,17 @@ class CsrfViewMiddleware(MiddlewareMixin):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
csrf_token = _sanitize_token(
|
cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
|
||||||
request.COOKIES[settings.CSRF_COOKIE_NAME])
|
|
||||||
# Use same token next time
|
|
||||||
request.META['CSRF_COOKIE'] = csrf_token
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
csrf_token = None
|
csrf_token = None
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
# Use same token next time.
|
||||||
|
request.META['CSRF_COOKIE'] = csrf_token
|
||||||
|
|
||||||
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
|
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
|
||||||
# bailing out, so that get_token still works
|
# bailing out, so that get_token still works
|
||||||
|
@ -142,7 +201,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
|
||||||
#
|
#
|
||||||
# The attacker will need to provide a CSRF cookie and token, but
|
# The attacker will need to provide a CSRF cookie and token, but
|
||||||
# that's no problem for a MITM and the session-independent
|
# that's no problem for a MITM and the session-independent
|
||||||
# nonce we're using. So the MITM can circumvent the CSRF
|
# secret we're using. So the MITM can circumvent the CSRF
|
||||||
# protection. This is true for any HTTP connection, but anyone
|
# protection. This is true for any HTTP connection, but anyone
|
||||||
# using HTTPS expects better! For this reason, for
|
# using HTTPS expects better! For this reason, for
|
||||||
# https://example.com/ we need additional protection that treats
|
# https://example.com/ we need additional protection that treats
|
||||||
|
@ -213,14 +272,16 @@ class CsrfViewMiddleware(MiddlewareMixin):
|
||||||
# and possible for PUT/DELETE.
|
# and possible for PUT/DELETE.
|
||||||
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
|
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
|
||||||
|
|
||||||
if not constant_time_compare(request_csrf_token, csrf_token):
|
request_csrf_token = _sanitize_token(request_csrf_token)
|
||||||
|
if not _compare_salted_tokens(request_csrf_token, csrf_token):
|
||||||
return self._reject(request, REASON_BAD_TOKEN)
|
return self._reject(request, REASON_BAD_TOKEN)
|
||||||
|
|
||||||
return self._accept(request)
|
return self._accept(request)
|
||||||
|
|
||||||
def process_response(self, request, response):
|
def process_response(self, request, response):
|
||||||
if getattr(response, 'csrf_processing_done', False):
|
if not getattr(request, 'csrf_cookie_needs_reset', False):
|
||||||
return response
|
if getattr(response, 'csrf_cookie_set', False):
|
||||||
|
return response
|
||||||
|
|
||||||
if not request.META.get("CSRF_COOKIE_USED", False):
|
if not request.META.get("CSRF_COOKIE_USED", False):
|
||||||
return response
|
return response
|
||||||
|
@ -237,5 +298,5 @@ class CsrfViewMiddleware(MiddlewareMixin):
|
||||||
)
|
)
|
||||||
# Content varies with the CSRF cookie, so set the Vary header.
|
# Content varies with the CSRF cookie, so set the Vary header.
|
||||||
patch_vary_headers(response, ('Cookie',))
|
patch_vary_headers(response, ('Cookie',))
|
||||||
response.csrf_processing_done = True
|
response.csrf_cookie_set = True
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -218,20 +218,25 @@ How it works
|
||||||
|
|
||||||
The CSRF protection is based on the following things:
|
The CSRF protection is based on the following things:
|
||||||
|
|
||||||
1. A CSRF cookie that is set to a random value (a session independent nonce, as
|
1. A CSRF cookie that is based on a random secret value, which other sites
|
||||||
it is called), which other sites will not have access to.
|
will not have access to.
|
||||||
|
|
||||||
This cookie is set by ``CsrfViewMiddleware``. It is meant to be permanent,
|
This cookie is set by ``CsrfViewMiddleware``. It is sent with every
|
||||||
but since there is no way to set a cookie that never expires, it is sent with
|
response that has called ``django.middleware.csrf.get_token()`` (the
|
||||||
every response that has called ``django.middleware.csrf.get_token()``
|
function used internally to retrieve the CSRF token), if it wasn't already
|
||||||
(the function used internally to retrieve the CSRF token).
|
set on the request.
|
||||||
|
|
||||||
For security reasons, the value of the CSRF cookie is changed each time a
|
In order to protect against `BREACH`_ attacks, the token is not simply the
|
||||||
|
secret; a random salt is prepended to the secret and used to scramble it.
|
||||||
|
|
||||||
|
For security reasons, the value of the secret is changed each time a
|
||||||
user logs in.
|
user logs in.
|
||||||
|
|
||||||
2. A hidden form field with the name 'csrfmiddlewaretoken' present in all
|
2. A hidden form field with the name 'csrfmiddlewaretoken' present in all
|
||||||
outgoing POST forms. The value of this field is the value of the CSRF
|
outgoing POST forms. The value of this field is, again, the value of the
|
||||||
cookie.
|
secret, with a salt which is both added to it and used to scramble it. The
|
||||||
|
salt is regenerated on every call to ``get_token()`` so that the form field
|
||||||
|
value is changed in every such response.
|
||||||
|
|
||||||
This part is done by the template tag.
|
This part is done by the template tag.
|
||||||
|
|
||||||
|
@ -239,6 +244,11 @@ The CSRF protection is based on the following things:
|
||||||
TRACE, a CSRF cookie must be present, and the 'csrfmiddlewaretoken' field
|
TRACE, a CSRF cookie must be present, and the 'csrfmiddlewaretoken' field
|
||||||
must be present and correct. If it isn't, the user will get a 403 error.
|
must be present and correct. If it isn't, the user will get a 403 error.
|
||||||
|
|
||||||
|
When validating the 'csrfmiddlewaretoken' field value, only the secret,
|
||||||
|
not the full token, is compared with the secret in the cookie value.
|
||||||
|
This allows the use of ever-changing tokens. While each request may use its
|
||||||
|
own token, the secret remains common to all.
|
||||||
|
|
||||||
This check is done by ``CsrfViewMiddleware``.
|
This check is done by ``CsrfViewMiddleware``.
|
||||||
|
|
||||||
4. In addition, for HTTPS requests, strict referer checking is done by
|
4. In addition, for HTTPS requests, strict referer checking is done by
|
||||||
|
@ -247,7 +257,7 @@ The CSRF protection is based on the following things:
|
||||||
application since that request won't come from your own exact domain.
|
application since that request won't come from your own exact domain.
|
||||||
|
|
||||||
This also addresses a man-in-the-middle attack that's possible under HTTPS
|
This also addresses a man-in-the-middle attack that's possible under HTTPS
|
||||||
when using a session independent nonce, due to the fact that HTTP
|
when using a session independent secret, due to the fact that HTTP
|
||||||
``Set-Cookie`` headers are (unfortunately) accepted by clients even when
|
``Set-Cookie`` headers are (unfortunately) accepted by clients even when
|
||||||
they are talking to a site under HTTPS. (Referer checking is not done for
|
they are talking to a site under HTTPS. (Referer checking is not done for
|
||||||
HTTP requests because the presence of the ``Referer`` header isn't reliable
|
HTTP requests because the presence of the ``Referer`` header isn't reliable
|
||||||
|
@ -283,6 +293,13 @@ vulnerability allows and much worse).
|
||||||
|
|
||||||
Checking against the :setting:`CSRF_COOKIE_DOMAIN` setting was added.
|
Checking against the :setting:`CSRF_COOKIE_DOMAIN` setting was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.10
|
||||||
|
|
||||||
|
Added salting to the token and started changing it with each request
|
||||||
|
to protect against `BREACH`_ attacks.
|
||||||
|
|
||||||
|
.. _BREACH: http://breachattack.com/
|
||||||
|
|
||||||
Caching
|
Caching
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
@ -499,15 +516,6 @@ No, this is by design. Not linking CSRF protection to a session allows using
|
||||||
the protection on sites such as a `pastebin` that allow submissions from
|
the protection on sites such as a `pastebin` that allow submissions from
|
||||||
anonymous users which don't have a session.
|
anonymous users which don't have a session.
|
||||||
|
|
||||||
Why not use a new token for each request?
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
Generating a new token for each request is problematic from a UI perspective
|
|
||||||
because it invalidates all previous forms. Most users would be very unhappy to
|
|
||||||
find that opening a new tab on your site has invalidated the form they had
|
|
||||||
just spent time filling out in another tab or that a form they accessed via
|
|
||||||
the back button could not be filled out.
|
|
||||||
|
|
||||||
Why might a user encounter a CSRF validation failure after logging in?
|
Why might a user encounter a CSRF validation failure after logging in?
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -118,13 +118,12 @@ GZip middleware
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
Security researchers recently revealed that when compression techniques
|
Security researchers recently revealed that when compression techniques
|
||||||
(including ``GZipMiddleware``) are used on a website, the site becomes
|
(including ``GZipMiddleware``) are used on a website, the site may become
|
||||||
exposed to a number of possible attacks. These approaches can be used to
|
exposed to a number of possible attacks. Before using ``GZipMiddleware`` on
|
||||||
compromise, among other things, Django's CSRF protection. Before using
|
your site, you should consider very carefully whether you are subject to
|
||||||
``GZipMiddleware`` on your site, you should consider very carefully whether
|
these attacks. If you're in *any* doubt about whether you're affected, you
|
||||||
you are subject to these attacks. If you're in *any* doubt about whether
|
should avoid using ``GZipMiddleware``. For more details, see the `the BREACH
|
||||||
you're affected, you should avoid using ``GZipMiddleware``. For more
|
paper (PDF)`_ and `breachattack.com`_.
|
||||||
details, see the `the BREACH paper (PDF)`_ and `breachattack.com`_.
|
|
||||||
|
|
||||||
.. _the BREACH paper (PDF): http://breachattack.com/resources/BREACH%20-%20SSL,%20gone%20in%2030%20seconds.pdf
|
.. _the BREACH paper (PDF): http://breachattack.com/resources/BREACH%20-%20SSL,%20gone%20in%2030%20seconds.pdf
|
||||||
.. _breachattack.com: http://breachattack.com
|
.. _breachattack.com: http://breachattack.com
|
||||||
|
@ -147,6 +146,12 @@ It will NOT compress content if any of the following are true:
|
||||||
You can apply GZip compression to individual views using the
|
You can apply GZip compression to individual views using the
|
||||||
:func:`~django.views.decorators.gzip.gzip_page()` decorator.
|
:func:`~django.views.decorators.gzip.gzip_page()` decorator.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.10
|
||||||
|
|
||||||
|
In older versions, Django's CSRF protection mechanism was vulnerable to
|
||||||
|
BREACH attacks when compression was used. This is no longer the case, but
|
||||||
|
you should still take care not to compromise your own secrets this way.
|
||||||
|
|
||||||
Conditional GET middleware
|
Conditional GET middleware
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
|
|
|
@ -256,6 +256,12 @@ CSRF
|
||||||
accepts an optional ``template_name`` parameter, defaulting to
|
accepts an optional ``template_name`` parameter, defaulting to
|
||||||
``'403_csrf.html'``, to control the template used to render the page.
|
``'403_csrf.html'``, to control the template used to render the page.
|
||||||
|
|
||||||
|
* To protect against `BREACH`_ attacks, the CSRF protection mechanism now
|
||||||
|
changes the form token value on every request (while keeping an invariant
|
||||||
|
secret which can be used to validate the different tokens).
|
||||||
|
|
||||||
|
.. _BREACH: http://breachattack.com/
|
||||||
|
|
||||||
Database backends
|
Database backends
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -795,6 +801,12 @@ Miscellaneous
|
||||||
* ``utils.version.get_version()`` returns :pep:`440` compliant release
|
* ``utils.version.get_version()`` returns :pep:`440` compliant release
|
||||||
candidate versions (e.g. '1.10rc1' instead of '1.10c1').
|
candidate versions (e.g. '1.10rc1' instead of '1.10c1').
|
||||||
|
|
||||||
|
* CSRF token values are now required to be strings of 64 alphanumerics; values
|
||||||
|
of 32 alphanumerics, as set by older versions of Django by default, are
|
||||||
|
automatically replaced by strings of 64 characters. Other values are
|
||||||
|
considered invalid. This should only affect developers or users who replace
|
||||||
|
these tokens.
|
||||||
|
|
||||||
* The ``LOGOUT_URL`` setting is removed as Django hasn't made use of it
|
* The ``LOGOUT_URL`` setting is removed as Django hasn't made use of it
|
||||||
since pre-1.0. If you use it in your project, you can add it to your
|
since pre-1.0. If you use it in your project, you can add it to your
|
||||||
project's settings. The default value was ``'/accounts/logout/'``.
|
project's settings. The default value was ``'/accounts/logout/'``.
|
||||||
|
|
|
@ -65,10 +65,10 @@ this if you know what you are doing. There are other :ref:`limitations
|
||||||
<csrf-limitations>` if your site has subdomains that are outside of your
|
<csrf-limitations>` if your site has subdomains that are outside of your
|
||||||
control.
|
control.
|
||||||
|
|
||||||
:ref:`CSRF protection works <how-csrf-works>` by checking for a nonce in each
|
:ref:`CSRF protection works <how-csrf-works>` by checking for a secret in each
|
||||||
POST request. This ensures that a malicious user cannot simply "replay" a form
|
POST request. This ensures that a malicious user cannot simply "replay" a form
|
||||||
POST to your website and have another logged in user unwittingly submit that
|
POST to your website and have another logged in user unwittingly submit that
|
||||||
form. The malicious user would have to know the nonce, which is user specific
|
form. The malicious user would have to know the secret, which is user specific
|
||||||
(using a cookie).
|
(using a cookie).
|
||||||
|
|
||||||
When deployed with :ref:`HTTPS <security-recommendation-ssl>`,
|
When deployed with :ref:`HTTPS <security-recommendation-ssl>`,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import json
|
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.middleware.csrf import _compare_salted_tokens as equivalent_tokens
|
||||||
from django.template.context_processors import csrf
|
from django.template.context_processors import csrf
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
@ -10,6 +9,7 @@ class TestContextProcessor(SimpleTestCase):
|
||||||
|
|
||||||
def test_force_text_on_token(self):
|
def test_force_text_on_token(self):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.META['CSRF_COOKIE'] = 'test-token'
|
test_token = '1bcdefghij2bcdefghij3bcdefghij4bcdefghij5bcdefghij6bcdefghijABCD'
|
||||||
|
request.META['CSRF_COOKIE'] = test_token
|
||||||
token = csrf(request).get('csrf_token')
|
token = csrf(request).get('csrf_token')
|
||||||
self.assertEqual(json.dumps(force_text(token)), '"test-token"')
|
self.assertTrue(equivalent_tokens(force_text(token), test_token))
|
||||||
|
|
|
@ -2,15 +2,20 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.middleware.csrf import (
|
from django.middleware.csrf import (
|
||||||
CSRF_KEY_LENGTH, CsrfViewMiddleware, get_token,
|
CSRF_TOKEN_LENGTH, CsrfViewMiddleware,
|
||||||
|
_compare_salted_tokens as equivalent_tokens, get_token,
|
||||||
)
|
)
|
||||||
from django.template import RequestContext, Template
|
from django.template import RequestContext, Template
|
||||||
from django.template.context_processors import csrf
|
from django.template.context_processors import csrf
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
from django.utils.encoding import force_bytes
|
||||||
|
from django.utils.six import text_type
|
||||||
from django.views.decorators.csrf import (
|
from django.views.decorators.csrf import (
|
||||||
csrf_exempt, ensure_csrf_cookie, requires_csrf_token,
|
csrf_exempt, ensure_csrf_cookie, requires_csrf_token,
|
||||||
)
|
)
|
||||||
|
@ -57,10 +62,7 @@ class TestingHttpRequest(HttpRequest):
|
||||||
|
|
||||||
|
|
||||||
class CsrfViewMiddlewareTest(SimpleTestCase):
|
class CsrfViewMiddlewareTest(SimpleTestCase):
|
||||||
# The csrf token is potentially from an untrusted source, so could have
|
_csrf_id = _csrf_id_cookie = '1bcdefghij2bcdefghij3bcdefghij4bcdefghij5bcdefghij6bcdefghijABCD'
|
||||||
# characters that need dealing with.
|
|
||||||
_csrf_id_cookie = b"<1>\xc2\xa1"
|
|
||||||
_csrf_id = "1"
|
|
||||||
|
|
||||||
def _get_GET_no_csrf_cookie_request(self):
|
def _get_GET_no_csrf_cookie_request(self):
|
||||||
return TestingHttpRequest()
|
return TestingHttpRequest()
|
||||||
|
@ -85,8 +87,24 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
|
||||||
req.POST['csrfmiddlewaretoken'] = self._csrf_id
|
req.POST['csrfmiddlewaretoken'] = self._csrf_id
|
||||||
return req
|
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):
|
def _check_token_present(self, response, csrf_id=None):
|
||||||
self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id))
|
text = text_type(response.content, response.charset)
|
||||||
|
match = re.search("name='csrfmiddlewaretoken' value='(.*?)'", text)
|
||||||
|
csrf_token = csrf_id or self._csrf_id
|
||||||
|
self.assertTrue(
|
||||||
|
match and equivalent_tokens(csrf_token, match.group(1)),
|
||||||
|
"Could not find csrfmiddlewaretoken to match %s" % csrf_token
|
||||||
|
)
|
||||||
|
|
||||||
def test_process_view_token_too_long(self):
|
def test_process_view_token_too_long(self):
|
||||||
"""
|
"""
|
||||||
|
@ -94,12 +112,45 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
|
||||||
created.
|
created.
|
||||||
"""
|
"""
|
||||||
req = self._get_GET_no_csrf_cookie_request()
|
req = self._get_GET_no_csrf_cookie_request()
|
||||||
req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000
|
req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 100000
|
||||||
CsrfViewMiddleware().process_view(req, token_view, (), {})
|
CsrfViewMiddleware().process_view(req, token_view, (), {})
|
||||||
resp = token_view(req)
|
resp = token_view(req)
|
||||||
resp2 = CsrfViewMiddleware().process_response(req, resp)
|
resp2 = CsrfViewMiddleware().process_response(req, resp)
|
||||||
csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
|
csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
|
||||||
self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH)
|
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):
|
def test_process_response_get_token_used(self):
|
||||||
"""
|
"""
|
||||||
|
@ -295,6 +346,37 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
|
||||||
csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME]
|
csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME]
|
||||||
self._check_token_present(resp, csrf_id=csrf_cookie.value)
|
self._check_token_present(resp, csrf_id=csrf_cookie.value)
|
||||||
|
|
||||||
|
def test_cookie_not_reset_on_accepted_request(self):
|
||||||
|
"""
|
||||||
|
The csrf token used in posts is changed on every request (although
|
||||||
|
stays equivalent). The csrf cookie should not change on accepted
|
||||||
|
requests. If it appears in the response, it should keep its value.
|
||||||
|
"""
|
||||||
|
req = self._get_POST_request_with_token()
|
||||||
|
CsrfViewMiddleware().process_view(req, token_view, (), {})
|
||||||
|
resp = token_view(req)
|
||||||
|
resp = CsrfViewMiddleware().process_response(req, resp)
|
||||||
|
csrf_cookie = resp.cookies.get(settings.CSRF_COOKIE_NAME, None)
|
||||||
|
if csrf_cookie:
|
||||||
|
self.assertEqual(
|
||||||
|
csrf_cookie.value, self._csrf_id_cookie,
|
||||||
|
"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)
|
@override_settings(DEBUG=True)
|
||||||
def test_https_bad_referer(self):
|
def test_https_bad_referer(self):
|
||||||
"""
|
"""
|
||||||
|
@ -592,7 +674,7 @@ class CsrfViewMiddlewareTest(SimpleTestCase):
|
||||||
|
|
||||||
POST = property(_get_post, _set_post)
|
POST = property(_get_post, _set_post)
|
||||||
|
|
||||||
token = 'ABC'
|
token = ('ABC' + self._csrf_id)[:CSRF_TOKEN_LENGTH]
|
||||||
|
|
||||||
req = CsrfPostRequest(token, raise_error=False)
|
req = CsrfPostRequest(token, raise_error=False)
|
||||||
resp = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
|
resp = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
|
||||||
|
|
|
@ -2,9 +2,13 @@
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from django.forms import CharField, Form, Media
|
from django.forms import CharField, Form, Media
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.middleware.csrf import CsrfViewMiddleware, get_token
|
from django.middleware.csrf import (
|
||||||
|
CsrfViewMiddleware, _compare_salted_tokens as equivalent_tokens, get_token,
|
||||||
|
)
|
||||||
from django.template import TemplateDoesNotExist, TemplateSyntaxError
|
from django.template import TemplateDoesNotExist, TemplateSyntaxError
|
||||||
from django.template.backends.dummy import TemplateStrings
|
from django.template.backends.dummy import TemplateStrings
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
@ -81,11 +85,10 @@ class TemplateStringsTests(SimpleTestCase):
|
||||||
template = self.engine.get_template('template_backends/csrf.html')
|
template = self.engine.get_template('template_backends/csrf.html')
|
||||||
content = template.render(request=request)
|
content = template.render(request=request)
|
||||||
|
|
||||||
expected = (
|
expected = '<input type="hidden" name="csrfmiddlewaretoken" value="([^"]+)" />'
|
||||||
'<input type="hidden" name="csrfmiddlewaretoken" '
|
match = re.match(expected, content) or re.match(expected.replace('"', "'"), content)
|
||||||
'value="{}" />'.format(get_token(request)))
|
self.assertTrue(match, "hidden csrftoken field not found in output")
|
||||||
|
self.assertTrue(equivalent_tokens(match.group(1), get_token(request)))
|
||||||
self.assertHTMLEqual(content, expected)
|
|
||||||
|
|
||||||
def test_no_directory_traversal(self):
|
def test_no_directory_traversal(self):
|
||||||
with self.assertRaises(TemplateDoesNotExist):
|
with self.assertRaises(TemplateDoesNotExist):
|
||||||
|
|
Loading…
Reference in New Issue