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

Reply via email to