Django in FIPS mode

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.

Leave a Reply

Your email address will not be published. Required fields are marked *