Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:deepdiff
User: [email protected]
Usertags: pu
* CVE-2025-58367: Class Pollution in Delta class
* CVE-2026-33155: Memory Exhaustion DoS through SAFE_TO_IMPORT
(Closes: #1131472)
diffstat for deepdiff-8.1.1 deepdiff-8.1.1
changelog | 9
patches/0001-Security-fix-Prevent-class-pollution-and-remote-code.patch | 246
++++++++++
patches/0002-Fix-CVE-2026-33155.patch | 147
+++++
patches/series | 2
4 files changed, 404 insertions(+)
diff -Nru deepdiff-8.1.1/debian/changelog deepdiff-8.1.1/debian/changelog
--- deepdiff-8.1.1/debian/changelog 2025-05-14 20:23:39.000000000 +0300
+++ deepdiff-8.1.1/debian/changelog 2026-07-02 19:38:08.000000000 +0300
@@ -1,3 +1,12 @@
+deepdiff (8.1.1-4+deb13u1) trixie; urgency=medium
+
+ * Non-maintainer upload.
+ * CVE-2025-58367: Class Pollution in Delta class
+ * CVE-2026-33155: Memory Exhaustion DoS through SAFE_TO_IMPORT
+ (Closes: #1131472)
+
+ -- Adrian Bunk <[email protected]> Thu, 02 Jul 2026 19:38:08 +0300
+
deepdiff (8.1.1-4) unstable; urgency=medium
* Team upload.
diff -Nru
deepdiff-8.1.1/debian/patches/0001-Security-fix-Prevent-class-pollution-and-remote-code.patch
deepdiff-8.1.1/debian/patches/0001-Security-fix-Prevent-class-pollution-and-remote-code.patch
---
deepdiff-8.1.1/debian/patches/0001-Security-fix-Prevent-class-pollution-and-remote-code.patch
1970-01-01 02:00:00.000000000 +0200
+++
deepdiff-8.1.1/debian/patches/0001-Security-fix-Prevent-class-pollution-and-remote-code.patch
2026-07-02 19:36:49.000000000 +0300
@@ -0,0 +1,246 @@
+From b73249762552ab0cc232e68b3c977b53bb5651f3 Mon Sep 17 00:00:00 2001
+From: Sep Dehpour <[email protected]>
+Date: Wed, 3 Sep 2025 09:42:30 -0700
+Subject: Security fix: Prevent class pollution and remote code execution in
+ Delta
+
+- Add validation to prevent traversing dunder attributes via check_elem()
+- Harden Delta class against malicious pickle payloads
+- Make SAFE_TO_IMPORT a frozenset for immutability
+- Add comprehensive security tests in test_security.py
+- Prevent access to __globals__ and other dangerous attributes
+---
+ deepdiff/delta.py | 8 ++-
+ deepdiff/path.py | 7 ++
+ deepdiff/serialization.py | 4 +-
+ tests/test_security.py | 133 ++++++++++++++++++++++++++++++++++++++
+ 4 files changed, 149 insertions(+), 3 deletions(-)
+ create mode 100644 tests/test_security.py
+
+diff --git a/deepdiff/delta.py b/deepdiff/delta.py
+index 8bafc9a..6d152f4 100644
+--- a/deepdiff/delta.py
++++ b/deepdiff/delta.py
+@@ -17,7 +17,7 @@ from deepdiff.helper import (
+ )
+ from deepdiff.path import (
+ _path_to_elements, _get_nested_obj, _get_nested_obj_and_force,
+- GET, GETATTR, parse_path, stringify_path,
++ GET, GETATTR, check_elem, parse_path, stringify_path,
+ )
+ from deepdiff.anyset import AnySet
+
+@@ -235,6 +235,11 @@ class Delta:
+ forced_old_value=None,
+ next_element=None,
+ ):
++ try:
++ check_elem(elem)
++ except ValueError as error:
++
self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path_for_err_reporting, error))
++ return not_found
+ # if forced_old_value is not None:
+ try:
+ if action == GET:
+@@ -517,6 +522,7 @@ class Delta:
+ obj = self
+ # obj = self.get_nested_obj(obj=self, elements=elements[:-1])
+ elem, action = elements[-1]
++ check_elem(elem)
+ except Exception as e:
+ self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e))
+ return None
+diff --git a/deepdiff/path.py b/deepdiff/path.py
+index ee63b5b..e5b64c7 100644
+--- a/deepdiff/path.py
++++ b/deepdiff/path.py
+@@ -117,6 +117,7 @@ def _path_to_elements(path,
root_element=DEFAULT_FIRST_ELEMENT):
+
+ def _get_nested_obj(obj, elements, next_element=None):
+ for (elem, action) in elements:
++ check_elem(elem)
+ if action == GET:
+ obj = obj[elem]
+ elif action == GETATTR:
+@@ -134,11 +135,17 @@ def _guess_type(elements, elem, index, next_element):
+ return {}
+
+
++def check_elem(elem):
++ if isinstance(elem, str) and elem.startswith("__") and
elem.endswith("__"):
++ raise ValueError("traversing dunder attributes is not allowed")
++
++
+ def _get_nested_obj_and_force(obj, elements, next_element=None):
+ prev_elem = None
+ prev_action = None
+ prev_obj = obj
+ for index, (elem, action) in enumerate(elements):
++ check_elem(elem)
+ _prev_obj = obj
+ if action == GET:
+ try:
+diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py
+index 4119742..9884c06 100644
+--- a/deepdiff/serialization.py
++++ b/deepdiff/serialization.py
+@@ -81,7 +81,7 @@ FORBIDDEN_MODULE_MSG = "Module '{}' is forbidden. You need
to explicitly pass it
+ DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT = 'report_repetition must be set
to True when ignore_order is True to create the delta object.'
+ DELTA_ERROR_WHEN_GROUP_BY = 'Delta can not be made when group_by is used
since the structure of data is modified from the original form.'
+
+-SAFE_TO_IMPORT = {
++SAFE_TO_IMPORT = frozenset({
+ 'builtins.range',
+ 'builtins.complex',
+ 'builtins.set',
+@@ -110,7 +110,7 @@ SAFE_TO_IMPORT = {
+ 'collections.OrderedDict',
+ 're.Pattern',
+ 'deepdiff.helper.Opcode',
+-}
++})
+
+
+ TYPE_STR_TO_TYPE = {
+diff --git a/tests/test_security.py b/tests/test_security.py
+new file mode 100644
+index 0000000..e221018
+--- /dev/null
++++ b/tests/test_security.py
+@@ -0,0 +1,133 @@
++import os
++import pickle
++import pytest
++from deepdiff import Delta
++from deepdiff.helper import Opcode
++from deepdiff.serialization import ForbiddenModule
++
++
++class TestDeltaClassPollution:
++
++ def test_builtins_int(self):
++
++ pollute_int = pickle.dumps(
++ {
++ "values_changed": {"root['tmp']": {"new_value": Opcode("", 0,
0, 0, 0)}},
++ "dictionary_item_added": {
++ (
++ ("root", "GETATTR"),
++ ("tmp", "GET"),
++ ("__repr__", "GETATTR"),
++ ("__globals__", "GETATTR"),
++ ("__builtins__", "GET"),
++ ("int", "GET"),
++ ): "no longer a class"
++ },
++ }
++ )
++
++ assert isinstance(pollute_int, bytes)
++
++ # ------------[ Exploit ]------------
++ # This could be some example, vulnerable, application.
++ # The inputs above could be sent via HTTP, for example.
++
++
++ # Existing dictionary; it is assumed that it contains
++ # at least one entry, otherwise a different Delta needs to be
++ # applied first, adding an entry to the dictionary.
++ mydict = {"tmp": "foobar"}
++
++ # Before pollution
++ assert 42 == int("41") + 1
++
++ # Apply Delta to mydict
++ result = mydict + Delta(pollute_int)
++
++ assert 1337 == int("1337")
++
++ def test_remote_code_execution(self):
++ if os.path.exists('/tmp/pwned'):
++ os.remove('/tmp/pwned')
++
++ pollute_safe_to_import = pickle.dumps(
++ {
++ "values_changed": {"root['tmp']": {"new_value": Opcode("", 0,
0, 0, 0)}},
++ "set_item_added": {
++ (
++ ("root", "GETATTR"),
++ ("tmp", "GET"),
++ ("__repr__", "GETATTR"),
++ ("__globals__", "GETATTR"),
++ ("sys", "GET"),
++ ("modules", "GETATTR"),
++ ("deepdiff.serialization", "GET"),
++ ("SAFE_TO_IMPORT", "GETATTR"),
++ ): set(["posix.system"])
++ },
++ }
++ )
++
++ # From https://davidhamann.de/2020/04/05/exploiting-python-pickle/
++ class RCE:
++ def __reduce__(self):
++ cmd = "id > /tmp/pwned"
++ return os.system, (cmd,)
++
++ # Wrap object with dictionary so that Delta does not crash
++ rce_pickle = pickle.dumps({"_": RCE()})
++
++ assert isinstance(pollute_safe_to_import, bytes)
++ assert isinstance(rce_pickle, bytes)
++
++ # ------------[ Exploit ]------------
++ # This could be some example, vulnerable, application.
++ # The inputs above could be sent via HTTP, for example.
++
++ # Existing dictionary; it is assumed that it contains
++ # at least one entry, otherwise a different Delta needs to be
++ # applied first, adding an entry to the dictionary.
++ mydict = {"tmp": "foobar"}
++
++ # Apply Delta to mydict
++ with pytest.raises(ValueError) as exc_info:
++ mydict + Delta(pollute_safe_to_import)
++ assert "traversing dunder attributes is not allowed" ==
str(exc_info.value)
++
++ with pytest.raises(ForbiddenModule) as exc_info:
++ Delta(rce_pickle) # no need to apply this Delta
++ assert "Module 'posix.system' is forbidden. You need to explicitly
pass it by passing a safe_to_import parameter" == str(exc_info.value)
++
++ assert not os.path.exists('/tmp/pwned'), "We should not have created
this file"
++
++ def test_delta_should_not_access_globals(self):
++
++ pollute_global = pickle.dumps(
++ {
++ "dictionary_item_added": {
++ (
++ ("root", "GETATTR"),
++ ("myfunc", "GETATTR"),
++ ("__globals__", "GETATTR"),
++ ("PWNED", "GET"),
++ ): 1337
++ }
++ }
++ )
++
++
++ # demo application
++ class Foo:
++ def __init__(self):
++ pass
++
++ def myfunc(self):
++ pass
++
++
++ PWNED = False
++ delta = Delta(pollute_global)
++ assert PWNED is False
++ b = Foo() + delta
++
++ assert PWNED is False
+--
+2.47.3
+
diff -Nru deepdiff-8.1.1/debian/patches/0002-Fix-CVE-2026-33155.patch
deepdiff-8.1.1/debian/patches/0002-Fix-CVE-2026-33155.patch
--- deepdiff-8.1.1/debian/patches/0002-Fix-CVE-2026-33155.patch 1970-01-01
02:00:00.000000000 +0200
+++ deepdiff-8.1.1/debian/patches/0002-Fix-CVE-2026-33155.patch 2026-07-02
19:36:49.000000000 +0300
@@ -0,0 +1,147 @@
+From 40df1ccd17ef5aace418832c0d07adf7254d1ae4 Mon Sep 17 00:00:00 2001
+From: Sep Dehpour <[email protected]>
+Date: Tue, 17 Mar 2026 10:52:13 -0700
+Subject: Fix (CVE-2026-33155)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+ deepdiff/serialization.py — Added a _SafeConstructor wrapper class that
intercepts calls to
+ size-sensitive constructors (like bytes and bytearray) during pickle
deserialization. When
+ find_class returns one of these types, it wraps it in _SafeConstructor,
which validates that no
+ integer argument exceeds _MAX_ALLOC_SIZE (128MB) before allowing the call.
This prevents payloads
+ like bytes(10**10) from causing memory exhaustion while still allowing
legitimate small
+ allocations.
+
+ Tests
+
+ tests/test_serialization.py — Added TestPicklingSecurity class with two
tests:
+ 1. test_restricted_unpickler_memory_exhaustion_cve — Reproduces the attack
with a crafted payload
+ attempting bytes(10_000_000_000), using resource.RLIMIT_AS to cap memory at
500MB as a safety net.
+ Verifies the fix raises UnpicklingError before any allocation.
+ 2. test_restricted_unpickler_allows_small_bytes — Ensures legitimate
bytes(100) payloads still
+ deserialize correctly.
+---
+ deepdiff/serialization.py | 35 ++++++++++++++++++++++++-
+ tests/test_serialization.py | 52 +++++++++++++++++++++++++++++++++++++
+ 2 files changed, 86 insertions(+), 1 deletion(-)
+
+diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py
+index 9884c06..e961b31 100644
+--- a/deepdiff/serialization.py
++++ b/deepdiff/serialization.py
+@@ -335,6 +335,35 @@ class SerializationMixin:
+ return "\n".join(f"{prefix}{r}" for r in result)
+
+
++# Maximum size allowed for integer arguments to constructors that allocate
++# memory proportional to the argument (e.g. bytes(n), bytearray(n)).
++# This prevents denial-of-service via crafted pickle payloads.
(CVE-2025-58367)
++_MAX_ALLOC_SIZE = 128 * 1024 * 1024 # 128 MB
++
++# Callables where an integer argument directly controls memory allocation
size.
++_SIZE_SENSITIVE_CALLABLES = frozenset({bytes, bytearray})
++
++
++class _SafeConstructor:
++ """Wraps a type constructor to prevent excessive memory allocation via
the REDUCE opcode."""
++ __slots__ = ('_wrapped',)
++
++ def __init__(self, wrapped):
++ self._wrapped = wrapped
++
++ def __call__(self, *args, **kwargs):
++ for arg in args:
++ if isinstance(arg, int) and arg > _MAX_ALLOC_SIZE:
++ raise pickle.UnpicklingError(
++ "Refusing to create {}() with size {}: "
++ "exceeds the maximum allowed size of {} bytes. "
++ "This could be a denial-of-service attack
payload.".format(
++ self._wrapped.__name__, arg, _MAX_ALLOC_SIZE
++ )
++ )
++ return self._wrapped(*args, **kwargs)
++
++
+ class _RestrictedUnpickler(pickle.Unpickler):
+
+ def __init__(self, *args, **kwargs):
+@@ -359,7 +388,11 @@ class _RestrictedUnpickler(pickle.Unpickler):
+ module_obj = sys.modules[module]
+ except KeyError:
+ raise
ModuleNotFoundError(MODULE_NOT_FOUND_MSG.format(module_dot_class)) from None
+- return getattr(module_obj, name)
++ cls = getattr(module_obj, name)
++ # Wrap size-sensitive callables to prevent DoS via large
allocations
++ if cls in _SIZE_SENSITIVE_CALLABLES:
++ return _SafeConstructor(cls)
++ return cls
+ # Forbid everything else.
+ raise ForbiddenModule(FORBIDDEN_MODULE_MSG.format(module_dot_class))
from None
+
+diff --git a/tests/test_serialization.py b/tests/test_serialization.py
+index 3c50683..e3d693c 100644
+--- a/tests/test_serialization.py
++++ b/tests/test_serialization.py
+@@ -137,6 +137,58 @@ class TestLoadContet:
+ load_path_content(path)
+
+
++class TestPicklingSecurity:
++
++ @pytest.mark.skipif(sys.platform == "win32", reason="Resource module is
Unix-only")
++ def test_restricted_unpickler_memory_exhaustion_cve(self):
++ """CVE-2026-33155: Prevent DoS via massive allocation through REDUCE
opcode.
++
++ The payload calls bytes(10_000_000_000) which is allowed by find_class
++ but would allocate ~9.3GB of memory. The fix should reject this before
++ the allocation happens.
++ """
++ import resource
++
++ # 1. Cap memory to 500MB to prevent system freezes during the test
++ soft, hard = resource.getrlimit(resource.RLIMIT_AS)
++ maxsize_bytes = 500 * 1024 * 1024
++ resource.setrlimit(resource.RLIMIT_AS, (maxsize_bytes, hard))
++
++ try:
++ # 2. Malicious payload: attempts to allocate ~9.3GB via
bytes(10000000000)
++ # This uses allowed builtins but passes a massive integer via
REDUCE
++ payload = (
++ b"(dp0\n"
++ b"S'_'\n"
++ b"cbuiltins\nbytes\n"
++ b"(I10000000000\n"
++ b"tR"
++ b"s."
++ )
++
++ # 3. After the patch, deepdiff should catch the size violation
++ # and raise UnpicklingError before attempting allocation.
++ with pytest.raises((ValueError, UnpicklingError)):
++ pickle_load(payload)
++ finally:
++ # Restore original memory limit so other tests are not affected
++ resource.setrlimit(resource.RLIMIT_AS, (soft, hard))
++
++ def test_restricted_unpickler_allows_small_bytes(self):
++ """Ensure legitimate small bytes objects can still be deserialized."""
++ # Payload: {'_': bytes(100)} — well within the 128MB limit
++ payload = (
++ b"(dp0\n"
++ b"S'_'\n"
++ b"cbuiltins\nbytes\n"
++ b"(I100\n"
++ b"tR"
++ b"s."
++ )
++ result = pickle_load(payload)
++ assert result == {'_': bytes(100)}
++
++
+ class TestPickling:
+
+ def test_serialize(self):
+--
+2.47.3
+
diff -Nru deepdiff-8.1.1/debian/patches/series
deepdiff-8.1.1/debian/patches/series
--- deepdiff-8.1.1/debian/patches/series 1970-01-01 02:00:00.000000000
+0200
+++ deepdiff-8.1.1/debian/patches/series 2026-07-02 19:38:02.000000000
+0300
@@ -0,0 +1,2 @@
+0001-Security-fix-Prevent-class-pollution-and-remote-code.patch
+0002-Fix-CVE-2026-33155.patch