Fixed #28401 -- Allowed hashlib.md5() calls to work with FIPS kernels.

md5 is not an approved algorithm in FIPS mode, and trying to instantiate
a hashlib.md5() will fail when the system is running in FIPS mode.

md5 is allowed when in a non-security context. There is a plan to add a
keyword parameter (usedforsecurity) to hashlib.md5() to annotate whether
or not the instance is being used in a security context.

In the case where it is not, the instantiation of md5 will be allowed.
See https://bugs.python.org/issue9216 for more details.

Some downstream python versions already support this parameter. To
support these versions, a new encapsulation of md5() has been added.
This encapsulation will pass through the usedforsecurity parameter in
the case where the parameter is supported, and strip it if it is not.

Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
Ade Lee 2021-08-10 18:13:54 -04:00 committed by Mariusz Felisiak
parent b1b26b37af
commit d10c7bfe56
10 changed files with 44 additions and 22 deletions

View File

@ -19,6 +19,7 @@ answer newbie questions, and generally made Django that much better:
Adam Johnson <https://github.com/adamchainz> Adam Johnson <https://github.com/adamchainz>
Adam Malinowski <https://adammalinowski.co.uk/> Adam Malinowski <https://adammalinowski.co.uk/>
Adam Vandenberg Adam Vandenberg
Ade Lee <alee@redhat.com>
Adiyat Mubarak <adiyatmubarak@gmail.com> Adiyat Mubarak <adiyatmubarak@gmail.com>
Adnan Umer <u.adnan@outlook.com> Adnan Umer <u.adnan@outlook.com>
Adrian Holovaty <adrian@holovaty.com> Adrian Holovaty <adrian@holovaty.com>

View File

@ -11,7 +11,7 @@ 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, constant_time_compare, get_random_string, pbkdf2, RANDOM_STRING_CHARS, constant_time_compare, get_random_string, md5, pbkdf2,
) )
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 _
@ -641,7 +641,7 @@ class MD5PasswordHasher(BasePasswordHasher):
def encode(self, password, salt): def encode(self, password, salt):
self._check_encode_args(password, salt) self._check_encode_args(password, salt)
hash = hashlib.md5((salt + password).encode()).hexdigest() hash = md5((salt + password).encode()).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash) return "%s$%s$%s" % (self.algorithm, salt, hash)
def decode(self, encoded): def decode(self, encoded):
@ -736,7 +736,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
def encode(self, password, salt): def encode(self, password, salt):
if salt != '': if salt != '':
raise ValueError('salt must be empty.') raise ValueError('salt must be empty.')
return hashlib.md5(password.encode()).hexdigest() return md5(password.encode()).hexdigest()
def decode(self, encoded): def decode(self, encoded):
return { return {

View File

@ -1,4 +1,3 @@
import hashlib
import json import json
import os import os
import posixpath import posixpath
@ -10,6 +9,7 @@ from django.contrib.staticfiles.utils import check_settings, matches_patterns
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage, get_storage_class from django.core.files.storage import FileSystemStorage, get_storage_class
from django.utils.crypto import md5
from django.utils.functional import LazyObject from django.utils.functional import LazyObject
@ -89,10 +89,10 @@ class HashedFilesMixin:
""" """
if content is None: if content is None:
return None return None
md5 = hashlib.md5() hasher = md5(usedforsecurity=False)
for chunk in content.chunks(): for chunk in content.chunks():
md5.update(chunk) hasher.update(chunk)
return md5.hexdigest()[:12] return hasher.hexdigest()[:12]
def hashed_name(self, name, content=None, filename=None): def hashed_name(self, name, content=None, filename=None):
# `filename` is the name of file to hash if `content` isn't given. # `filename` is the name of file to hash if `content` isn't given.

View File

@ -1,6 +1,5 @@
"File-based cache backend" "File-based cache backend"
import glob import glob
import hashlib
import os import os
import pickle import pickle
import random import random
@ -11,6 +10,7 @@ import zlib
from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache
from django.core.files import locks from django.core.files import locks
from django.core.files.move import file_move_safe from django.core.files.move import file_move_safe
from django.utils.crypto import md5
class FileBasedCache(BaseCache): class FileBasedCache(BaseCache):
@ -128,8 +128,10 @@ class FileBasedCache(BaseCache):
root cache path joined with the md5sum of the key and a suffix. root cache path joined with the md5sum of the key and a suffix.
""" """
key = self.make_and_validate_key(key, version=version) key = self.make_and_validate_key(key, version=version)
return os.path.join(self._dir, ''.join( return os.path.join(self._dir, ''.join([
[hashlib.md5(key.encode()).hexdigest(), self.cache_suffix])) md5(key.encode(), usedforsecurity=False).hexdigest(),
self.cache_suffix,
]))
def clear(self): def clear(self):
""" """

View File

@ -1,10 +1,10 @@
import hashlib from django.utils.crypto import md5
TEMPLATE_FRAGMENT_KEY_TEMPLATE = 'template.cache.%s.%s' TEMPLATE_FRAGMENT_KEY_TEMPLATE = 'template.cache.%s.%s'
def make_template_fragment_key(fragment_name, vary_on=None): def make_template_fragment_key(fragment_name, vary_on=None):
hasher = hashlib.md5() hasher = md5(usedforsecurity=False)
if vary_on is not None: if vary_on is not None:
for arg in vary_on: for arg in vary_on:
hasher.update(str(arg).encode()) hasher.update(str(arg).encode())

View File

@ -22,6 +22,7 @@ from django.db.backends.base.base import (
) )
from django.utils import timezone from django.utils import timezone
from django.utils.asyncio import async_unsafe from django.utils.asyncio import async_unsafe
from django.utils.crypto import md5
from django.utils.dateparse import parse_datetime, parse_time from django.utils.dateparse import parse_datetime, parse_time
from django.utils.duration import duration_microseconds from django.utils.duration import duration_microseconds
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
@ -233,7 +234,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
create_deterministic_function('LN', 1, none_guard(math.log)) create_deterministic_function('LN', 1, none_guard(math.log))
create_deterministic_function('LOG', 2, none_guard(lambda x, y: math.log(y, x))) create_deterministic_function('LOG', 2, none_guard(lambda x, y: math.log(y, x)))
create_deterministic_function('LPAD', 3, _sqlite_lpad) create_deterministic_function('LPAD', 3, _sqlite_lpad)
create_deterministic_function('MD5', 1, none_guard(lambda x: hashlib.md5(x.encode()).hexdigest())) create_deterministic_function('MD5', 1, none_guard(lambda x: md5(x.encode()).hexdigest()))
create_deterministic_function('MOD', 2, none_guard(math.fmod)) create_deterministic_function('MOD', 2, none_guard(math.fmod))
create_deterministic_function('PI', 0, lambda: math.pi) create_deterministic_function('PI', 0, lambda: math.pi)
create_deterministic_function('POWER', 2, none_guard(operator.pow)) create_deterministic_function('POWER', 2, none_guard(operator.pow))

View File

@ -1,12 +1,12 @@
import datetime import datetime
import decimal import decimal
import functools import functools
import hashlib
import logging import logging
import time import time
from contextlib import contextmanager from contextlib import contextmanager
from django.db import NotSupportedError from django.db import NotSupportedError
from django.utils.crypto import md5
logger = logging.getLogger('django.db.backends') logger = logging.getLogger('django.db.backends')
@ -216,7 +216,7 @@ def names_digest(*args, length):
Generate a 32-bit digest of a set of arguments that can be used to shorten Generate a 32-bit digest of a set of arguments that can be used to shorten
identifying names. identifying names.
""" """
h = hashlib.md5() h = md5(usedforsecurity=False)
for arg in args: for arg in args:
h.update(arg.encode()) h.update(arg.encode())
return h.hexdigest()[:length] return h.hexdigest()[:length]

View File

@ -1,7 +1,6 @@
import argparse import argparse
import ctypes import ctypes
import faulthandler import faulthandler
import hashlib
import io import io
import itertools import itertools
import logging import logging
@ -26,6 +25,7 @@ from django.test.utils import (
setup_databases as _setup_databases, setup_test_environment, setup_databases as _setup_databases, setup_test_environment,
teardown_databases as _teardown_databases, teardown_test_environment, teardown_databases as _teardown_databases, teardown_test_environment,
) )
from django.utils.crypto import new_hash
from django.utils.datastructures import OrderedSet from django.utils.datastructures import OrderedSet
from django.utils.deprecation import RemovedInDjango50Warning from django.utils.deprecation import RemovedInDjango50Warning
@ -509,7 +509,7 @@ class Shuffler:
@classmethod @classmethod
def _hash_text(cls, text): def _hash_text(cls, text):
h = hashlib.new(cls.hash_algorithm) h = new_hash(cls.hash_algorithm, usedforsecurity=False)
h.update(text.encode('utf-8')) h.update(text.encode('utf-8'))
return h.hexdigest() return h.hexdigest()

View File

@ -16,13 +16,13 @@ cache keys to prevent delivery of wrong content.
An example: i18n middleware would need to distinguish caches by the An example: i18n middleware would need to distinguish caches by the
"Accept-language" header. "Accept-language" header.
""" """
import hashlib
import time import time
from collections import defaultdict from collections import defaultdict
from django.conf import settings from django.conf import settings
from django.core.cache import caches from django.core.cache import caches
from django.http import HttpResponse, HttpResponseNotModified from django.http import HttpResponse, HttpResponseNotModified
from django.utils.crypto import md5
from django.utils.http import ( from django.utils.http import (
http_date, parse_etags, parse_http_date_safe, quote_etag, http_date, parse_etags, parse_http_date_safe, quote_etag,
) )
@ -118,7 +118,9 @@ def get_max_age(response):
def set_response_etag(response): def set_response_etag(response):
if not response.streaming and response.content: if not response.streaming and response.content:
response.headers['ETag'] = quote_etag(hashlib.md5(response.content).hexdigest()) response.headers['ETag'] = quote_etag(
md5(response.content, usedforsecurity=False).hexdigest(),
)
return response return response
@ -325,12 +327,12 @@ def _i18n_cache_key_suffix(request, cache_key):
def _generate_cache_key(request, method, headerlist, key_prefix): def _generate_cache_key(request, method, headerlist, key_prefix):
"""Return a cache key from the headers given in the header list.""" """Return a cache key from the headers given in the header list."""
ctx = hashlib.md5() ctx = md5(usedforsecurity=False)
for header in headerlist: for header in headerlist:
value = request.META.get(header) value = request.META.get(header)
if value is not None: if value is not None:
ctx.update(value.encode()) ctx.update(value.encode())
url = hashlib.md5(request.build_absolute_uri().encode('ascii')) url = md5(request.build_absolute_uri().encode('ascii'), usedforsecurity=False)
cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % ( cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % (
key_prefix, method, url.hexdigest(), ctx.hexdigest()) key_prefix, method, url.hexdigest(), ctx.hexdigest())
return _i18n_cache_key_suffix(request, cache_key) return _i18n_cache_key_suffix(request, cache_key)
@ -338,7 +340,7 @@ def _generate_cache_key(request, method, headerlist, key_prefix):
def _generate_cache_header_key(key_prefix, request): def _generate_cache_header_key(key_prefix, request):
"""Return a cache key for the header cache.""" """Return a cache key for the header cache."""
url = hashlib.md5(request.build_absolute_uri().encode('ascii')) url = md5(request.build_absolute_uri().encode('ascii'), usedforsecurity=False)
cache_key = 'views.decorators.cache.cache_header.%s.%s' % ( cache_key = 'views.decorators.cache.cache_header.%s.%s' % (
key_prefix, url.hexdigest()) key_prefix, url.hexdigest())
return _i18n_cache_key_suffix(request, cache_key) return _i18n_cache_key_suffix(request, cache_key)

View File

@ -7,6 +7,7 @@ import secrets
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.inspect import func_supports_parameter
class InvalidAlgorithm(ValueError): class InvalidAlgorithm(ValueError):
@ -74,3 +75,18 @@ def pbkdf2(password, salt, iterations, dklen=0, digest=None):
password = force_bytes(password) password = force_bytes(password)
salt = force_bytes(salt) salt = force_bytes(salt)
return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen) return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen)
# TODO: Remove when dropping support for PY38. inspect.signature() is used to
# detect whether the usedforsecurity argument is available as this fix may also
# have been applied by downstream package maintainers to other versions in
# their repositories.
if func_supports_parameter(hashlib.md5, 'usedforsecurity'):
md5 = hashlib.md5
new_hash = hashlib.new
else:
def md5(data=b'', *, usedforsecurity=True):
return hashlib.md5(data)
def new_hash(hash_algorithm, *, usedforsecurity=True):
return hashlib.new(hash_algorithm)