Django today (2.2+) still uses the MD5 hash function to generate cache keys and database object names. They're not security related, so that's fine. But if you're running on a FIPS-compliant system then MD5 is disabled and Django blows up.
Red Hat Enterprise Linux provides a patched version of Python which allows you to pass a keyword argument to md5, "usedforsecurity". If you set that to False the system will allow your call to MD5 to go through.
This extension is being merged upstream so everyone will have it, it's targeted for Python 3.9: https://github.com/python/cpython/pull/16044. Once that work is complete, Django will update their code to pass the parameter natively and none of this will be necessary: https://code.djangoproject.com/ticket/28401. But until then....
So, RHEL has a supported method to tell the system that you're using MD5 for non-security purposes; now we need to make Django pass that parameter without forking Django. We can do this with monkey patching.
I would prefer not to use a global monkey patch. It would work, but then anything using MD5 would be allowed instead of only the things I've verified are non-security related. This could be a compliance issue.
Implementing a localized monkey patch isn't too much harder once you know a little Python voodoo. We load a copy of the hashlib module and then monkey-patch the copy to pass the "usedforsecurity" parameter. Then we inject the monkey-patched version into any modules we need to replace their normal hashlib object with our copy.
import hashlib
import importlib
def _non_security_md5(*args, **kwargs):
kwargs['usedforsecurity'] = False
return hashlib.md5(*args, **kwargs)
def monkey_patch_md5(modules_to_patch):
"""Monkey-patch calls to MD5 that aren't used for security purposes.
Sets RHEL's custom flag `usedforsecurity` to False allowing MD5 in FIPS mode.
`modules_to_patch` must be an iterable of module names (strings).
Modules must use `import hashlib` and not `from hashlib import md5`.
"""
# Manually load a module as a unique instance
# https://stackoverflow.com/questions/11170949/how-to-make-a-copy-of-a-python-module-at-runtime
HASHLIB_SPEC = importlib.util.find_spec('hashlib')
patched_hashlib = importlib.util.module_from_spec(HASHLIB_SPEC)
HASHLIB_SPEC.loader.exec_module(patched_hashlib)
patched_hashlib.md5 = _non_security_md5 # Monkey patch MD5
# Inject our patched_hashlib for all requested modules
for module_name in modules_to_patch:
module = importlib.import_module(module_name)
module.hashlib = patched_hashlib
When our application starts up it detects FIPS mode and runs the monkey patch:
modules_to_patch = [
'django.contrib.staticfiles.storage',
'django.core.cache.backends.filebased',
'django.core.cache.utils',
'django.db.backends.utils',
# 'django.db.backends.sqlite3.base', -- Only if system has sqlite installed
'django.utils.cache',
]
try:
import hashlib
hashlib.md5()
except ValueError:
monkey_patch_md5(modules_to_patch)
This lists the specific modules in Django that use MD5 for non-security purposes (determined by searching through the codebase and reading the code). Each module has its version of hashlib replaced with the monkey-patched version and Django is none the wiser.
Hi,
I'm facing the same issue, but not sure how to apply this patch, meaning in which file should I put the above. ? Can you please guide here.
Regards,
Bharath Singh
I've placed the final code snippet in my settings files used when deploying Django to our production systems. The rest of the code can live in any other file and get imported into the settings file.
For example:
/fips_monkey_patch.py
# contains the code block that starts with "import hashlib"
/settings.py
import fips_monkey_patch
# contains the code block that starts with "modules_to_patch"
Kyle - I just want to say that I ran into the exact same issue with Starlette and was able to apply your "monkey patch" technique with 100% success. Thank you for taking the time to document your solution!