Fix CVE-2025-69872 in diskcache =============================== Install diskcache-community and read your caches with :class:`SafeDisk `: .. code-block:: shell $ pip install diskcache-community .. code-block:: python 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 :class:`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 :func:`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``. :doc:`migrating` covers the dependency, the imports and the Django settings. 2. **Select SafeDisk for caches you create.** Pass it as ``disk`` to :class:`Cache `, :class:`FanoutCache `, :class:`Deque ` and :class:`Index `, and as the ``DISK`` option of :class:`DjangoCache `: .. code-block:: python CACHES = { 'default': { 'BACKEND': 'diskcache_community.DjangoCache', 'LOCATION': '/var/tmp/django_cache', 'DISK': 'diskcache_community.SafeDisk', } } :class:`SafeDisk ` reads the same files as :class:`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. :ref:`tutorial-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: .. code-block:: shell $ DISKCACHE_DISK=safe python app.py or in code, before the caches are created: .. code-block:: python 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 :func:`pickle.load` until it moves to the fork. A library that passes its own disk keeps it. :ref:`tutorial-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 :meth:`SafeDisk.with_types ` instead of falling back to :class:`Disk `: .. code-block:: python 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: .. code-block:: python 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 :class:`Disk ` may hold objects that :class:`SafeDisk ` refuses, and reading them raises :exc:`pickle.UnpicklingError`. If the entries can be computed again, clear the cache once with :meth:`clear `, or let reads drop them with :meth:`with_options `: .. code-block:: python 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 :ref:`tutorial-unreadable-entries`. 6. **Check the result.** ``type(cache.disk)`` names the disk a cache uses, and :func:`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 :class:`Disk ` still reads pickles with :func:`pickle.load`, as 5.6.3 does, so every existing cache keeps opening and caches of arbitrary objects keep working. :class:`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 :class:`SafeDisk `. What a restricted disk has to cover ----------------------------------- Some projects subclass :class:`Disk ` and replace :func:`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 :class:`SafeDisk ` does each one: * **Keys as well as values.** Keys that are not plain strings, bytes or numbers are pickled too, and :meth:`Disk.get ` unpickles the ``key`` column whenever the cache returns a key, for example while iterating or in :meth:`peekitem `. A disk that restricts only :meth:`fetch ` still runs a crafted key. :class:`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 :meth:`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. :class:`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``. :meth:`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. :meth:`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. :class:`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. :class:`SafeDisk ` checks the names in a value's pickle against the same allowlist before anything is written and raises :exc:`TypeError` naming the class. To report a problem with :class:`SafeDisk `, follow ``SECURITY.md`` in the repository. .. _`CVE-2025-69872`: https://www.cve.org/CVERecord?id=CVE-2025-69872