From 887128abe5e510341cc4a82f6914a82d5ff1b6a7 Mon Sep 17 00:00:00 2001
From: Sep Dehpour <sep@zepworks.com>
Date: Tue, 17 Mar 2026 10:52:13 -0700
Subject: [PATCH 1/4] Fix (CVE-2025-58367)
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(-)

Index: deepdiff-6.3.0/deepdiff/serialization.py
===================================================================
--- deepdiff-6.3.0.orig/deepdiff/serialization.py
+++ deepdiff-6.3.0/deepdiff/serialization.py
@@ -272,6 +272,35 @@ class SerializationMixin:
         return '\n'.join(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-2026-33155)
+_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):
@@ -296,7 +325,11 @@ class _RestrictedUnpickler(pickle.Unpick
                 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
 
Index: deepdiff-6.3.0/tests/test_serialization.py
===================================================================
--- deepdiff-6.3.0.orig/tests/test_serialization.py
+++ deepdiff-6.3.0/tests/test_serialization.py
@@ -114,6 +114,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):
