Fixed #31291 -- Renamed salt to mask for CSRF tokens.

This commit is contained in:
Ram Rachum 2020-02-25 15:16:19 +02:00 committed by GitHub
parent 271fdab8b7
commit 5b09354954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 26 additions and 26 deletions

View File

@ -42,33 +42,33 @@ def _get_new_csrf_string():
return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS) return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)
def _salt_cipher_secret(secret): def _mask_cipher_secret(secret):
""" """
Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a 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. token by adding a mask and applying it to the secret.
""" """
salt = _get_new_csrf_string() mask = _get_new_csrf_string()
chars = CSRF_ALLOWED_CHARS chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt)) pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in mask))
cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs) cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
return salt + cipher return mask + cipher
def _unsalt_cipher_token(token): def _unmask_cipher_token(token):
""" """
Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length 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 CSRF_TOKEN_LENGTH, and that its first half is a mask), use it to decrypt
the second half to produce the original secret. the second half to produce the original secret.
""" """
salt = token[:CSRF_SECRET_LENGTH] mask = token[:CSRF_SECRET_LENGTH]
token = token[CSRF_SECRET_LENGTH:] token = token[CSRF_SECRET_LENGTH:]
chars = CSRF_ALLOWED_CHARS chars = CSRF_ALLOWED_CHARS
pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt)) pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in mask))
return ''.join(chars[x - y] for x, y in pairs) # Note negative values are ok return ''.join(chars[x - y] for x, y in pairs) # Note negative values are ok
def _get_new_csrf_token(): def _get_new_csrf_token():
return _salt_cipher_secret(_get_new_csrf_string()) return _mask_cipher_secret(_get_new_csrf_string())
def get_token(request): def get_token(request):
@ -83,11 +83,11 @@ def get_token(request):
""" """
if "CSRF_COOKIE" not in request.META: if "CSRF_COOKIE" not in request.META:
csrf_secret = _get_new_csrf_string() csrf_secret = _get_new_csrf_string()
request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret) request.META["CSRF_COOKIE"] = _mask_cipher_secret(csrf_secret)
else: else:
csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"]) csrf_secret = _unmask_cipher_token(request.META["CSRF_COOKIE"])
request.META["CSRF_COOKIE_USED"] = True request.META["CSRF_COOKIE_USED"] = True
return _salt_cipher_secret(csrf_secret) return _mask_cipher_secret(csrf_secret)
def rotate_token(request): def rotate_token(request):
@ -111,20 +111,20 @@ def _sanitize_token(token):
elif len(token) == CSRF_SECRET_LENGTH: elif len(token) == CSRF_SECRET_LENGTH:
# Older Django versions set cookies to values of CSRF_SECRET_LENGTH # Older Django versions set cookies to values of CSRF_SECRET_LENGTH
# alphanumeric characters. For backwards compatibility, accept # alphanumeric characters. For backwards compatibility, accept
# such values as unsalted secrets. # such values as unmasked secrets.
# It's easier to salt here and be consistent later, rather than add # It's easier to mask here and be consistent later, rather than add
# different code paths in the checks, although that might be a tad more # different code paths in the checks, although that might be a tad more
# efficient. # efficient.
return _salt_cipher_secret(token) return _mask_cipher_secret(token)
return _get_new_csrf_token() return _get_new_csrf_token()
def _compare_salted_tokens(request_csrf_token, csrf_token): def _compare_masked_tokens(request_csrf_token, csrf_token):
# Assume both arguments are sanitized -- that is, strings of # Assume both arguments are sanitized -- that is, strings of
# length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS. # length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
return constant_time_compare( return constant_time_compare(
_unsalt_cipher_token(request_csrf_token), _unmask_cipher_token(request_csrf_token),
_unsalt_cipher_token(csrf_token), _unmask_cipher_token(csrf_token),
) )
@ -306,7 +306,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '') request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
request_csrf_token = _sanitize_token(request_csrf_token) request_csrf_token = _sanitize_token(request_csrf_token)
if not _compare_salted_tokens(request_csrf_token, csrf_token): if not _compare_masked_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)

View File

@ -239,15 +239,15 @@ The CSRF protection is based on the following things:
set on the request. set on the request.
In order to protect against `BREACH`_ attacks, the token is not simply the 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. secret; a random mask is prepended to the secret and used to scramble it.
For security reasons, the value of the secret is changed each time a For security reasons, the value of the secret is changed each time a
user logs in. user logs in.
#. A hidden form field with the name 'csrfmiddlewaretoken' present in all #. A hidden form field with the name 'csrfmiddlewaretoken' present in all
outgoing POST forms. The value of this field is, again, the value of the outgoing POST forms. The value of this field is, again, the value of the
secret, with a salt which is both added to it and used to scramble it. The secret, with a mask 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 mask is regenerated on every call to ``get_token()`` so that the form field
value is changed in every such response. value is changed in every such response.
This part is done by the template tag. This part is done by the template tag.

View File

@ -1,5 +1,5 @@
from django.http import HttpRequest from django.http import HttpRequest
from django.middleware.csrf import _compare_salted_tokens as equivalent_tokens from django.middleware.csrf import _compare_masked_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

View File

@ -7,7 +7,7 @@ from django.http import HttpRequest, HttpResponse
from django.middleware.csrf import ( from django.middleware.csrf import (
CSRF_SESSION_KEY, CSRF_TOKEN_LENGTH, REASON_BAD_TOKEN, CSRF_SESSION_KEY, CSRF_TOKEN_LENGTH, REASON_BAD_TOKEN,
REASON_NO_CSRF_COOKIE, CsrfViewMiddleware, REASON_NO_CSRF_COOKIE, CsrfViewMiddleware,
_compare_salted_tokens as equivalent_tokens, get_token, _compare_masked_tokens as equivalent_tokens, get_token,
) )
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
from django.views.decorators.csrf import csrf_exempt, requires_csrf_token from django.views.decorators.csrf import csrf_exempt, requires_csrf_token

View File

@ -3,7 +3,7 @@ import re
from django.forms import CharField, Form, Media from django.forms import CharField, Form, Media
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.middleware.csrf import ( from django.middleware.csrf import (
CsrfViewMiddleware, _compare_salted_tokens as equivalent_tokens, get_token, CsrfViewMiddleware, _compare_masked_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