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) .. important:: :class:`JSONDisk ` does not protect reads. It writes JSON, but it reads each row as the row's ``mode`` column says, so a crafted row holding a pickle is unpickled as with the default :class:`Disk `. Use :class:`SafeJSONDisk `, which reads the same files and never reads a pickle. 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. Dash's ``DiskcacheManager`` takes only an upstream cache; see :ref:`the note on Dash `. 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. Use :meth:`SafeDisk.with_numpy ` to store arrays instead, which saves them in numpy's ``.npy`` format and combines with ``with_types``: .. code-block:: python from diskcache_community import Cache, SafeDisk ArrayDisk = SafeDisk.with_numpy().with_types(Message, Point) cache = Cache('/var/tmp/mycache', disk=ArrayDisk) cache['embedding'] = array To select it from the environment, name a class like ``ArrayDisk`` in ``DISKCACHE_DISK``, such as ``myapp.cache:ArrayDisk``. The same lists apply. 5. **Deal with entries already in the cache.** A cache written with the default :class:`Disk ` may hold objects that :class:`SafeDisk ` refuses; those entries can be cleared or treated as missing (: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. :class:`JSONDisk ` writes no pickles, but it still reads one from a crafted row. If you use it, select :class:`SafeJSONDisk `, which reads the same files and never reads a pickle. :class:`SafeDisk ` does not decode what :class:`JSONDisk ` wrote. See :ref:`tutorial-safejsondisk`. 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.** Non-trivial keys are pickled too, and unpickled whenever the cache returns a 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``, and reads, deletes, expiry and culling all follow that path. :class:`SafeDisk ` refuses any path that leaves the cache directory, on reads and on removal. * **Exact classes, not module prefixes.** A pickle can name a global through a module that merely imports it. :meth:`SafeDisk.with_types ` accepts a name only when every step is a class defined in a trusted module, and it refuses dunder attributes. * **No numpy, scipy, torch, bitstruct or pyahocorasick.** Crafted pickles of their objects can corrupt memory or run code. :meth:`SafeDisk.with_types ` refuses their classes; use :meth:`with_numpy ` to store arrays instead. * **No imports while reading.** :class:`SafeDisk ` looks classes up only in modules that are already imported. * **Refuse on write.** A value the reader would refuse fails when it is written, with :exc:`TypeError` naming the class, not when a later process reads it. 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