Version in base suite: 8.1.1-4 Base version: deepdiff_8.1.1-4 Target version: deepdiff_8.1.1-4+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/d/deepdiff/deepdiff_8.1.1-4.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/d/deepdiff/deepdiff_8.1.1-4+deb13u1.dsc 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(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpdob57tvq/deepdiff_8.1.1-4.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpdob57tvq/deepdiff_8.1.1-4+deb13u1.dsc: no acceptable signature found diff -Nru deepdiff-8.1.1/debian/changelog deepdiff-8.1.1/debian/changelog --- deepdiff-8.1.1/debian/changelog 2025-05-14 17:23:39.000000000 +0000 +++ deepdiff-8.1.1/debian/changelog 2026-07-02 16:38:08.000000000 +0000 @@ -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 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 00:00:00.000000000 +0000 +++ deepdiff-8.1.1/debian/patches/0001-Security-fix-Prevent-class-pollution-and-remote-code.patch 2026-07-02 16:36:49.000000000 +0000 @@ -0,0 +1,246 @@ +From b73249762552ab0cc232e68b3c977b53bb5651f3 Mon Sep 17 00:00:00 2001 +From: Sep Dehpour +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 00:00:00.000000000 +0000 +++ deepdiff-8.1.1/debian/patches/0002-Fix-CVE-2026-33155.patch 2026-07-02 16:36:49.000000000 +0000 @@ -0,0 +1,147 @@ +From 40df1ccd17ef5aace418832c0d07adf7254d1ae4 Mon Sep 17 00:00:00 2001 +From: Sep Dehpour +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 00:00:00.000000000 +0000 +++ deepdiff-8.1.1/debian/patches/series 2026-07-02 16:38:02.000000000 +0000 @@ -0,0 +1,2 @@ +0001-Security-fix-Prevent-class-pollution-and-remote-code.patch +0002-Fix-CVE-2026-33155.patch