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
/tmpor 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¶
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.Select SafeDisk for caches you create. Pass it as
disktoCache,FanoutCache,DequeandIndex, and as theDISKoption ofDjangoCache:CACHES = { 'default': { 'BACKEND': 'diskcache_community.DjangoCache', 'LOCATION': '/var/tmp/django_cache', 'DISK': 'diskcache_community.SafeDisk', } }
SafeDiskreads the same files asDiskbut unpickles only standard-library types such asdict,list,datetimeandDecimal, 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.Select SafeDisk for caches that libraries create. A cache created without a
diskargument 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 upstreamdiskcachepackage keeps reading pickles withpickle.load()until it moves to the fork. A library that passes its own disk keeps it. Default disk in the tutorial has the details.Allow your own classes. If the cache holds dataclasses, named tuples, enums or pydantic models, allow them with
SafeDisk.with_typesinstead of falling back toDisk: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 intoDISKCACHE_SAFE_TYPESandDISKCACHE_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)
Deal with entries already in the cache. A cache written with the default
Diskmay hold objects thatSafeDiskrefuses, and reading them raisespickle.UnpicklingError. If the entries can be computed again, clear the cache once withclear, or let reads drop them withwith_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_communitylogger. See Unreadable entries.Check the result.
type(cache.disk)names the disk a cache uses, anddefault_disk()returns the process default. Runpip show diskcacheto see whether the upstream package is still installed, and, with pipdeptree installed,pipdeptree -r -p diskcacheto 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.getunpickles thekeycolumn whenever the cache returns a key, for example while iterating or inpeekitem. A disk that restricts onlyfetchstill runs a crafted key.SafeDiskreads 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, andDisk.removealso 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.SafeDiskrefuses 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.typestherefore also trusts whatever those modules import: a name likeos.systemorbuiltins.evalresolves through any trusted module that importsosorbuiltins.SafeDisk.with_typesaccepts 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_typesrefuses 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.
SafeDisklooks 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.
SafeDiskchecks the names in a value’s pickle against the same allowlist before anything is written and raisesTypeErrornaming the class.
To report a problem with SafeDisk,
follow SECURITY.md in the repository.