1
0
mirror of https://github.com/django/django.git synced 2025-08-26 11:49:12 +00:00

Fixed #36546 -- Deprecated django.utils.crypto.constant_time_compare() in favor of hmac.compare_digest().

Signed-off-by: SaJH <wogur981208@gmail.com>
This commit is contained in:
SaJH 2025-08-22 15:32:09 +02:00 committed by Sarah Boyce
parent 3ba24c18e7
commit 0246f47888
9 changed files with 48 additions and 24 deletions

View File

@ -1,3 +1,4 @@
import hmac
import inspect import inspect
import re import re
import warnings import warnings
@ -6,7 +7,6 @@ from django.apps import apps as django_apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.middleware.csrf import rotate_token from django.middleware.csrf import rotate_token
from django.utils.crypto import constant_time_compare
from django.utils.deprecation import RemovedInDjango61Warning from django.utils.deprecation import RemovedInDjango61Warning
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.views.decorators.debug import sensitive_variables from django.views.decorators.debug import sensitive_variables
@ -175,7 +175,7 @@ def login(request, user, backend=None):
if SESSION_KEY in request.session: if SESSION_KEY in request.session:
if _get_user_session_key(request) != user.pk or ( if _get_user_session_key(request) != user.pk or (
session_auth_hash session_auth_hash
and not constant_time_compare( and not hmac.compare_digest(
request.session.get(HASH_SESSION_KEY, ""), session_auth_hash request.session.get(HASH_SESSION_KEY, ""), session_auth_hash
) )
): ):
@ -217,7 +217,7 @@ async def alogin(request, user, backend=None):
if await request.session.ahas_key(SESSION_KEY): if await request.session.ahas_key(SESSION_KEY):
if await _aget_user_session_key(request) != user.pk or ( if await _aget_user_session_key(request) != user.pk or (
session_auth_hash session_auth_hash
and not constant_time_compare( and not hmac.compare_digest(
await request.session.aget(HASH_SESSION_KEY, ""), await request.session.aget(HASH_SESSION_KEY, ""),
session_auth_hash, session_auth_hash,
) )
@ -323,7 +323,7 @@ def get_user(request):
session_hash_verified = False session_hash_verified = False
else: else:
session_auth_hash = user.get_session_auth_hash() session_auth_hash = user.get_session_auth_hash()
session_hash_verified = constant_time_compare( session_hash_verified = hmac.compare_digest(
session_hash, session_auth_hash session_hash, session_auth_hash
) )
if not session_hash_verified: if not session_hash_verified:
@ -331,7 +331,7 @@ def get_user(request):
# with the fallback secrets and stop when a matching one is # with the fallback secrets and stop when a matching one is
# found. # found.
if session_hash and any( if session_hash and any(
constant_time_compare(session_hash, fallback_auth_hash) hmac.compare_digest(session_hash, fallback_auth_hash)
for fallback_auth_hash in user.get_session_auth_fallback_hash() for fallback_auth_hash in user.get_session_auth_fallback_hash()
): ):
request.session.cycle_key() request.session.cycle_key()
@ -364,7 +364,7 @@ async def aget_user(request):
session_hash_verified = False session_hash_verified = False
else: else:
session_auth_hash = user.get_session_auth_hash() session_auth_hash = user.get_session_auth_hash()
session_hash_verified = constant_time_compare( session_hash_verified = hmac.compare_digest(
session_hash, session_auth_hash session_hash, session_auth_hash
) )
if not session_hash_verified: if not session_hash_verified:
@ -372,7 +372,7 @@ async def aget_user(request):
# with the fallback secrets and stop when a matching one is # with the fallback secrets and stop when a matching one is
# found. # found.
if session_hash and any( if session_hash and any(
constant_time_compare(session_hash, fallback_auth_hash) hmac.compare_digest(session_hash, fallback_auth_hash)
for fallback_auth_hash in user.get_session_auth_fallback_hash() for fallback_auth_hash in user.get_session_auth_fallback_hash()
): ):
await request.session.acycle_key() await request.session.acycle_key()

View File

@ -2,6 +2,7 @@ import base64
import binascii import binascii
import functools import functools
import hashlib import hashlib
import hmac
import importlib import importlib
import math import math
import warnings import warnings
@ -12,12 +13,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.signals import setting_changed from django.core.signals import setting_changed
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.crypto import ( from django.utils.crypto import RANDOM_STRING_CHARS, get_random_string, pbkdf2
RANDOM_STRING_CHARS,
constant_time_compare,
get_random_string,
pbkdf2,
)
from django.utils.encoding import force_bytes, force_str from django.utils.encoding import force_bytes, force_str
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.translation import gettext_noop as _ from django.utils.translation import gettext_noop as _
@ -349,7 +345,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
def verify(self, password, encoded): def verify(self, password, encoded):
decoded = self.decode(encoded) decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded["salt"], decoded["iterations"]) encoded_2 = self.encode(password, decoded["salt"], decoded["iterations"])
return constant_time_compare(encoded, encoded_2) return hmac.compare_digest(encoded, encoded_2)
def safe_summary(self, encoded): def safe_summary(self, encoded):
decoded = self.decode(encoded) decoded = self.decode(encoded)
@ -533,7 +529,7 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
algorithm, data = encoded.split("$", 1) algorithm, data = encoded.split("$", 1)
assert algorithm == self.algorithm assert algorithm == self.algorithm
encoded_2 = self.encode(password, data.encode("ascii")) encoded_2 = self.encode(password, data.encode("ascii"))
return constant_time_compare(encoded, encoded_2) return hmac.compare_digest(encoded, encoded_2)
def safe_summary(self, encoded): def safe_summary(self, encoded):
decoded = self.decode(encoded) decoded = self.decode(encoded)
@ -628,7 +624,7 @@ class ScryptPasswordHasher(BasePasswordHasher):
decoded["block_size"], decoded["block_size"],
decoded["parallelism"], decoded["parallelism"],
) )
return constant_time_compare(encoded, encoded_2) return hmac.compare_digest(encoded, encoded_2)
def safe_summary(self, encoded): def safe_summary(self, encoded):
decoded = self.decode(encoded) decoded = self.decode(encoded)
@ -681,7 +677,7 @@ class MD5PasswordHasher(BasePasswordHasher):
def verify(self, password, encoded): def verify(self, password, encoded):
decoded = self.decode(encoded) decoded = self.decode(encoded)
encoded_2 = self.encode(password, decoded["salt"]) encoded_2 = self.encode(password, decoded["salt"])
return constant_time_compare(encoded, encoded_2) return hmac.compare_digest(encoded, encoded_2)
def safe_summary(self, encoded): def safe_summary(self, encoded):
decoded = self.decode(encoded) decoded = self.decode(encoded)

View File

@ -1,7 +1,8 @@
import hmac
from datetime import datetime from datetime import datetime
from django.conf import settings from django.conf import settings
from django.utils.crypto import constant_time_compare, salted_hmac from django.utils.crypto import salted_hmac
from django.utils.http import base36_to_int, int_to_base36 from django.utils.http import base36_to_int, int_to_base36
@ -67,7 +68,7 @@ class PasswordResetTokenGenerator:
# Check that the timestamp/uid has not been tampered with # Check that the timestamp/uid has not been tampered with
for secret in [self.secret, *self.secret_fallbacks]: for secret in [self.secret, *self.secret_fallbacks]:
if constant_time_compare( if hmac.compare_digest(
self._make_token_with_timestamp(user, ts, secret), self._make_token_with_timestamp(user, ts, secret),
token, token,
): ):

View File

@ -36,12 +36,13 @@ These functions make use of all of them.
import base64 import base64
import datetime import datetime
import hmac
import json import json
import time import time
import zlib import zlib
from django.conf import settings from django.conf import settings
from django.utils.crypto import constant_time_compare, salted_hmac from django.utils.crypto import salted_hmac
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
@ -209,7 +210,7 @@ class Signer:
raise BadSignature('No "%s" found in value' % self.sep) raise BadSignature('No "%s" found in value' % self.sep)
value, sig = signed_value.rsplit(self.sep, 1) value, sig = signed_value.rsplit(self.sep, 1)
for key in [self.key, *self.fallback_keys]: for key in [self.key, *self.fallback_keys]:
if constant_time_compare(sig, self.signature(value, key)): if hmac.compare_digest(sig, self.signature(value, key)):
return value return value
raise BadSignature('Signature "%s" does not match' % sig) raise BadSignature('Signature "%s" does not match' % sig)

View File

@ -5,6 +5,7 @@ This module provides a middleware that implements protection
against request forgeries from other sites. against request forgeries from other sites.
""" """
import hmac
import logging import logging
import string import string
from collections import defaultdict from collections import defaultdict
@ -15,7 +16,7 @@ from django.core.exceptions import DisallowedHost, ImproperlyConfigured
from django.http import HttpHeaders, UnreadablePostError from django.http import HttpHeaders, UnreadablePostError
from django.urls import get_callable from django.urls import get_callable
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
from django.utils.crypto import constant_time_compare, get_random_string from django.utils.crypto import get_random_string
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.http import is_same_domain from django.utils.http import is_same_domain
@ -154,7 +155,7 @@ def _does_token_match(request_csrf_token, csrf_secret):
if len(request_csrf_token) == CSRF_TOKEN_LENGTH: if len(request_csrf_token) == CSRF_TOKEN_LENGTH:
request_csrf_token = _unmask_cipher_token(request_csrf_token) request_csrf_token = _unmask_cipher_token(request_csrf_token)
assert len(request_csrf_token) == CSRF_SECRET_LENGTH assert len(request_csrf_token) == CSRF_SECRET_LENGTH
return constant_time_compare(request_csrf_token, csrf_secret) return hmac.compare_digest(request_csrf_token, csrf_secret)
class RejectRequest(Exception): class RejectRequest(Exception):

View File

@ -5,8 +5,10 @@ Django's standard crypto functions and utilities.
import hashlib import hashlib
import hmac import hmac
import secrets import secrets
import warnings
from django.conf import settings from django.conf import settings
from django.utils.deprecation import RemovedInDjango70Warning
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -64,7 +66,12 @@ def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
def constant_time_compare(val1, val2): def constant_time_compare(val1, val2):
"""Return True if the two strings are equal, False otherwise.""" """Return True if the two strings are equal, False otherwise."""
return secrets.compare_digest(force_bytes(val1), force_bytes(val2)) warnings.warn(
"constant_time_compare() is deprecated. Use hmac.compare_digest() instead.",
RemovedInDjango70Warning,
stacklevel=2,
)
return hmac.compare_digest(val1, val2)
def pbkdf2(password, salt, iterations, dklen=0, digest=None): def pbkdf2(password, salt, iterations, dklen=0, digest=None):

View File

@ -53,6 +53,8 @@ details on these changes.
* The ``django.core.mail.forbid_multi_line_headers()`` and * The ``django.core.mail.forbid_multi_line_headers()`` and
``django.core.mail.message.sanitize_address()`` functions will be removed. ``django.core.mail.message.sanitize_address()`` functions will be removed.
* The ``django.utils.crypto.constant_time_compare()`` function will be removed.
.. _deprecation-removed-in-6.1: .. _deprecation-removed-in-6.1:
6.1 6.1

View File

@ -570,6 +570,9 @@ Miscellaneous
* The undocumented ``django.core.mail.forbid_multi_line_headers()`` and * The undocumented ``django.core.mail.forbid_multi_line_headers()`` and
``django.core.mail.message.sanitize_address()`` functions are deprecated. ``django.core.mail.message.sanitize_address()`` functions are deprecated.
* The ``django.utils.crypto.constant_time_compare()`` function is deprecated
because it is merely an alias of :py:func:`hmac.compare_digest`.
Features removed in 6.0 Features removed in 6.0
======================= =======================

View File

@ -2,15 +2,19 @@ import hashlib
import unittest import unittest
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.test.utils import ignore_warnings
from django.utils.crypto import ( from django.utils.crypto import (
InvalidAlgorithm, InvalidAlgorithm,
constant_time_compare, constant_time_compare,
pbkdf2, pbkdf2,
salted_hmac, salted_hmac,
) )
from django.utils.deprecation import RemovedInDjango70Warning
class TestUtilsCryptoMisc(SimpleTestCase): class TestUtilsCryptoMisc(SimpleTestCase):
# RemovedInDjango70Warning.
@ignore_warnings(category=RemovedInDjango70Warning)
def test_constant_time_compare(self): def test_constant_time_compare(self):
# It's hard to test for constant time, just test the result. # It's hard to test for constant time, just test the result.
self.assertTrue(constant_time_compare(b"spam", b"spam")) self.assertTrue(constant_time_compare(b"spam", b"spam"))
@ -18,6 +22,15 @@ class TestUtilsCryptoMisc(SimpleTestCase):
self.assertTrue(constant_time_compare("spam", "spam")) self.assertTrue(constant_time_compare("spam", "spam"))
self.assertFalse(constant_time_compare("spam", "eggs")) self.assertFalse(constant_time_compare("spam", "eggs"))
def test_constant_time_compare_deprecated(self):
msg = (
"constant_time_compare() is deprecated. "
"Use hmac.compare_digest() instead."
)
with self.assertWarnsMessage(RemovedInDjango70Warning, msg) as ctx:
constant_time_compare(b"spam", b"spam")
self.assertEqual(ctx.filename, __file__)
def test_salted_hmac(self): def test_salted_hmac(self):
tests = [ tests = [
((b"salt", b"value"), {}, "b51a2e619c43b1ca4f91d15c57455521d71d61eb"), ((b"salt", b"value"), {}, "b51a2e619c43b1ca4f91d15c57455521d71d61eb"),