Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Changes
=======

6.0.0 (NEXT)
-------------

**Breaking Changes**

* Pickle deserialization is now restricted to safe built-in types only.
This mitigates CVE-2025-69872, which allowed arbitrary code execution
when an attacker with write access to the cache directory injected a
crafted pickle payload.

The following types are permitted during deserialization:

- Python builtins: ``int``, ``float``, ``str``, ``bytes``, ``bytearray``,
``list``, ``dict``, ``tuple``, ``set``, ``frozenset``, ``complex``,
``range``, ``slice``, ``object``, ``bool``, ``None``
- ``collections``: ``OrderedDict``, ``defaultdict``, ``deque``
- ``datetime``: ``date``, ``datetime``, ``time``, ``timedelta``,
``timezone``
- ``decimal.Decimal``
- ``fractions.Fraction``
- ``uuid.UUID``

All other types will raise ``UnpicklingError`` on read.

* There is no opt-out mechanism. Users who need to cache custom types have
two migration paths:

1. Use ``JSONDisk`` for JSON-serializable data::

cache = Cache('/tmp/my-cache', disk=JSONDisk)

2. Subclass ``Disk`` and override ``get()`` and ``fetch()`` with a custom
serialization strategy appropriate for your data.

**New Features**

* Added ``SafeUnpickler`` class for restricted pickle deserialization.
* Added ``UnpicklingError`` exception raised when a disallowed type is
encountered during deserialization.

**Internal**

* ``SAFE_PICKLE_CLASSES`` uses ``frozenset`` values to prevent runtime
modification.

5.6.3 (2023-08-31)
-------------------

* Previous release (see git history for details).
8 changes: 6 additions & 2 deletions diskcache/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
Disk,
EmptyDirWarning,
JSONDisk,
SafeUnpickler,
Timeout,
UnknownFileWarning,
UnpicklingError,
)
from .fanout import FanoutCache
from .persistent import Deque, Index
Expand Down Expand Up @@ -44,9 +46,11 @@
'JSONDisk',
'Lock',
'RLock',
'SafeUnpickler',
'Timeout',
'UNKNOWN',
'UnknownFileWarning',
'UnpicklingError',
'barrier',
'memoize_stampede',
'throttle',
Expand All @@ -61,8 +65,8 @@
pass

__title__ = 'diskcache'
__version__ = '5.6.3'
__build__ = 0x050603
__version__ = '6.0.0'
__build__ = 0x060000
__author__ = 'Grant Jenks'
__license__ = 'Apache 2.0'
__copyright__ = 'Copyright 2016-2023 Grant Jenks'
149 changes: 146 additions & 3 deletions diskcache/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,149 @@ def __repr__(self):
}


class UnpicklingError(pickle.UnpicklingError):
"""Error raised when unpickling encounters a disallowed type."""


# Safe modules and classes that are allowed during deserialization.
# These are standard Python types that cannot execute arbitrary code
# during unpickling. This structure is immutable to prevent runtime
# modification as a security bypass.
SAFE_PICKLE_CLASSES = {
'builtins': frozenset(
{
'True',
'False',
'None',
'bytes',
'bytearray',
'complex',
'dict',
'float',
'frozenset',
'int',
'list',
'object',
'range',
'set',
'slice',
'str',
'tuple',
}
),
# Python 2 module name used by pickle protocols 0 and 1.
'__builtin__': frozenset(
{
'True',
'False',
'None',
'bytes',
'bytearray',
'complex',
'dict',
'float',
'frozenset',
'int',
'list',
'long',
'object',
'range',
'set',
'slice',
'str',
'tuple',
'unicode',
'xrange',
}
),
'collections': frozenset(
{
'OrderedDict',
'defaultdict',
'deque',
}
),
# Used by pickle protocols 0 and 1 for object reconstruction.
'copy_reg': frozenset(
{
'_reconstructor',
}
),
'copyreg': frozenset(
{
'_reconstructor',
}
),
'datetime': frozenset(
{
'date',
'datetime',
'time',
'timedelta',
'timezone',
}
),
'decimal': frozenset(
{
'Decimal',
}
),
'fractions': frozenset(
{
'Fraction',
}
),
'uuid': frozenset(
{
'UUID',
}
),
'_codecs': frozenset(
{
'encode',
}
),
}


class SafeUnpickler(pickle.Unpickler):
"""Restricted unpickler that only allows safe built-in types.

This prevents arbitrary code execution via crafted pickle payloads.
Only types listed in SAFE_PICKLE_CLASSES are permitted.

"""

def find_class(self, module, name):
"""Only allow safe classes to be unpickled.

:param str module: module name
:param str name: class/function name
:raises UnpicklingError: if the class is not in the allowlist

"""
allowed = SAFE_PICKLE_CLASSES.get(module, frozenset())
if name in allowed:
return super().find_class(module, name)
raise UnpicklingError(
'Unpickling of {}.{} is not allowed. '
'Only safe built-in types can be deserialized. '
'Use JSONDisk or a custom Disk subclass for other types.'.format(
module, name
)
)


def safe_pickle_load(file_obj):
"""Load a pickle from a file object using the restricted unpickler.

:param file_obj: file-like object to read from
:return: deserialized Python object

"""
return SafeUnpickler(file_obj).load()


class Disk:
"""Cache key and value serialization for SQLite database and files."""

Expand Down Expand Up @@ -174,7 +317,7 @@ def get(self, key, raw):
if raw:
return bytes(key) if type(key) is sqlite3.Binary else key
else:
return pickle.load(io.BytesIO(key))
return safe_pickle_load(io.BytesIO(key))

def store(self, value, read, key=UNKNOWN):
"""Convert `value` to fields size, mode, filename, and value for Cache
Expand Down Expand Up @@ -279,9 +422,9 @@ def fetch(self, mode, filename, value, read):
elif mode == MODE_PICKLE:
if value is None:
with open(op.join(self._directory, filename), 'rb') as reader:
return pickle.load(reader)
return safe_pickle_load(reader)
else:
return pickle.load(io.BytesIO(value))
return safe_pickle_load(io.BytesIO(value))

def filename(self, key=UNKNOWN, value=UNKNOWN):
"""Return filename and full-path tuple for file storage.
Expand Down
Loading