Fix CVE-2025-69872 in diskcache

Install diskcache-community and read your caches with SafeDisk:

$ pip install diskcache-community
from diskcache_community import Cache, SafeDisk

cache = Cache('/var/tmp/mycache', disk=SafeDisk)

The rest of this page explains what the vulnerability is, who is exposed, the steps for caches you create and for caches libraries create for you, and what a restricted disk has to cover if you write your own.

What the vulnerability is

CVE-2025-69872 (CWE-502) affects every release of the upstream diskcache package, 5.6.3 included, and there is no fixed upstream release. The default Disk stores keys and values that are not plain strings, bytes or numbers as pickles, in cache.db and in the value files next to it, and reads them back with pickle.load(). A pickle can name any function to call while it is loaded, so anyone who can write into the cache directory can run code in every process that reads from it.

The attacker needs write access to the cache directory. You are exposed when someone other than the user running the process can write there, for example:

  • a cache directory under /tmp or another directory shared between users;

  • a cache on a shared volume or network drive, or one mounted into a container that other workloads can write to;

  • a cache restored from a CI cache, an artifact or a download that someone else could have produced.

A cache that only its own user can write to is not exposed. diskcache-community creates new cache directories with mode 0700 and leaves existing ones as they are, so check the permissions of directories created by older versions.

Steps

  1. Move to diskcache-community. The fork keeps the API and the files on disk, and is imported as diskcache_community. Migrating from diskcache 5.6.3 covers the dependency, the imports and the Django settings.

  2. Select SafeDisk for caches you create. Pass it as disk to Cache, FanoutCache, Deque and Index, and as the DISK option of DjangoCache:

    CACHES = {
        'default': {
            'BACKEND': 'diskcache_community.DjangoCache',
            'LOCATION': '/var/tmp/django_cache',
            'DISK': 'diskcache_community.SafeDisk',
        }
    }
    

    SafeDisk reads the same files as Disk but unpickles only standard-library types such as dict, list, datetime and Decimal, refuses any other key or value when it is written, and refuses file paths outside the cache directory. SafeDisk in the tutorial lists the types.

  3. Select SafeDisk for caches that libraries create. A cache created without a disk argument uses the process default, which you can set without changing the library’s code:

    $ DISKCACHE_DISK=safe python app.py
    

    or in code, before the caches are created:

    import diskcache_community
    
    diskcache_community.configure(diskcache_community.SafeDisk)
    

    This reaches only caches created through diskcache_community. A library that imports the upstream diskcache package keeps reading pickles with pickle.load() until it moves to the fork. A library that passes its own disk keeps it. Default disk in the tutorial has the details.

  4. Allow your own classes. If the cache holds dataclasses, named tuples, enums or pydantic models, allow them with SafeDisk.with_types instead of falling back to Disk:

    from diskcache_community import Cache, SafeDisk
    
    from myapp.models import Message, Point
    
    AppDisk = SafeDisk.with_types(Message, Point, modules=('myapp.schemas',))
    cache = Cache('/var/tmp/mycache', disk=AppDisk)
    

    With DISKCACHE_DISK=safe, the same lists go into DISKCACHE_SAFE_TYPES and DISKCACHE_SAFE_MODULES. numpy classes cannot be allowed, because a crafted numpy pickle can corrupt memory. Store arrays as bytes instead:

    import io
    
    import numpy
    
    buffer = io.BytesIO()
    numpy.save(buffer, array, allow_pickle=False)
    cache['embedding'] = buffer.getvalue()
    
    array = numpy.load(io.BytesIO(cache['embedding']), allow_pickle=False)
    
  5. Deal with entries already in the cache. A cache written with the default Disk may hold objects that SafeDisk refuses, and reading them raises pickle.UnpicklingError. If the entries can be computed again, clear the cache once with clear, or let reads drop them with with_options:

    AppDisk = SafeDisk.with_options(on_error='miss')
    

    An unreadable entry is then deleted and treated as missing, and the first one is logged as a warning on the diskcache_community logger. See Unreadable entries.

  6. Check the result. type(cache.disk) names the disk a cache uses, and default_disk() returns the process default. Run pip show diskcache to see whether the upstream package is still installed, and, with pipdeptree installed, pipdeptree -r -p diskcache to see which dependency requires it.

What stays the same

The default Disk still reads pickles with pickle.load(), as 5.6.3 does, so every existing cache keeps opening and caches of arbitrary objects keep working. SafeDisk is a choice you make per cache or per process, and the choice is never stored in the cache directory: every process that opens a directory decides for itself.

Vulnerability scanners match on the distribution name. pip-audit, Dependabot and similar tools report CVE-2025-69872 for diskcache and nothing for diskcache-community, whichever disk you use. A clean scan means the upstream package is gone, not that your caches use SafeDisk.

What a restricted disk has to cover

Some projects subclass Disk and replace pickle.load() with a restricted unpickler of their own. Restricting how values are unpickled is not enough. A disk that closes this vulnerability covers all of the following, and SafeDisk does each one:

  • Keys as well as values. Keys that are not plain strings, bytes or numbers are pickled too, and Disk.get unpickles the key column whenever the cache returns a key, for example while iterating or in peekitem. A disk that restricts only fetch still runs a crafted key. SafeDisk reads keys with the same unpickler as values.

  • The filename column. Large values live in files whose path is stored in cache.db. Reads, deletes, expiry and culling all follow that path, and Disk.remove also removes the parent directories it leaves empty. A crafted row can therefore point outside the cache and have a file read or deleted, including by a delete that runs after a failed read. SafeDisk refuses any path that leaves the cache directory, on reads and on removal.

  • Exact classes, not module prefixes. With protocol 4 and later, which the writer of the pickle chooses, a pickle names a global as a dotted path that the unpickler follows attribute by attribute. Trusting every name under a package such as myapp.types therefore also trusts whatever those modules import: a name like os.system or builtins.eval resolves through any trusted module that imports os or builtins. SafeDisk.with_types accepts a name only when every step is a class and the final class is defined in a trusted module, and it refuses dunder attributes.

  • No numpy. numpy’s array and dtype reconstructors restore internal dtype state from the pickle without checking it, so a crafted array can make numpy read arbitrary memory as object pointers. Allowing those names turns the cache into a crash or memory-corruption vector. SafeDisk.with_types refuses numpy classes even when asked for them.

  • No imports while reading. Resolving a name by importing its module runs that module’s top-level code, chosen by the pickle. SafeDisk looks classes up only in modules that are already imported.

  • Refuse on write. A value the reader would refuse should fail when it is written, not when a later process reads it. SafeDisk checks the names in a value’s pickle against the same allowlist before anything is written and raises TypeError naming the class.

To report a problem with SafeDisk, follow SECURITY.md in the repository.