CVE-2025-58367
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
deepdiffPyPI | >= 5.0.0, < 8.6.1 | 8.6.1 |
Affected products
1Patches
516 files changed · +178 −14
AUTHORS.md+1 −0 modified@@ -75,3 +75,4 @@ Authors in order of the timeline of their contributions: - [dtorres-sf](https://github.com/dtorres-sf) for the fix for moving nested tables when using iterable_compare_func. - [Jim Cipar](https://github.com/jcipar) for the fix recursion depth limit when hashing numpy.datetime64 - [Enji Cooper](https://github.com/ngie-eign) for converting legacy setuptools use to pyproject.toml +- [Diogo Correia](https://github.com/diogotcorreia) for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution.
.bumpversion.cfg+1 −1 modified@@ -1,5 +1,5 @@ [bumpversion] -current_version = 8.6.0 +current_version = 8.6.1 commit = True tag = True tag_name = {new_version}
CHANGELOG.md+4 −0 modified@@ -1,5 +1,9 @@ # DeepDiff Change log +- v8-6-1 + - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + + - v8-6-0 - Added Colored View thanks to @mauvilsa - Added support for applying deltas to NamedTuple thanks to @paulsc
CITATION.cff+1 −1 modified@@ -5,6 +5,6 @@ authors: given-names: "Sep" orcid: "https://orcid.org/0009-0009-5828-4345" title: "DeepDiff" -version: 8.6.0 +version: 8.6.1 date-released: 2024 url: "https://github.com/seperman/deepdiff"
deepdiff/delta.py+7 −1 modified@@ -17,7 +17,7 @@ ) 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 from deepdiff.summarize import summarize @@ -237,6 +237,11 @@ def _get_elem_and_compare_to_old_value( 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: @@ -536,6 +541,7 @@ def _get_elements_and_details(self, path): obj = self # obj = self.get_nested_obj(obj=self, elements=elements[:-1]) elem, action = elements[-1] # type: ignore + check_elem(elem) except Exception as e: self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e)) return None
deepdiff/__init__.py+1 −1 modified@@ -1,6 +1,6 @@ """This module offers the DeepDiff, DeepSearch, grep, Delta and DeepHash classes.""" # flake8: noqa -__version__ = '8.6.0' +__version__ = '8.6.1' import logging if __name__ == '__main__':
deepdiff/path.py+7 −0 modified@@ -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:
deepdiff/serialization.py+2 −2 modified@@ -59,7 +59,7 @@ class UnsupportedFormatErr(TypeError): 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', @@ -95,7 +95,7 @@ class UnsupportedFormatErr(TypeError): 'ipaddress.IPv4Address', 'ipaddress.IPv6Address', 'collections.abc.KeysView', -} +}) TYPE_STR_TO_TYPE = {
docs/authors.rst+1 −0 modified@@ -117,6 +117,7 @@ and polars support. limit when hashing numpy.datetime64 - `Enji Cooper <https://github.com/ngie-eign>`__ for converting legacy setuptools use to pyproject.toml +- `Diogo Correia <https://github.com/diogotcorreia>`__ for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution. .. _Sep Dehpour (Seperman): http://www.zepworks.com
docs/changelog.rst+3 −0 modified@@ -5,6 +5,9 @@ Changelog DeepDiff Changelog +- v8-6-1 + - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + - v8-6-0 - Added Colored View thanks to @mauvilsa - Added support for applying deltas to NamedTuple thanks to @paulsc
docs/conf.py+2 −2 modified@@ -64,9 +64,9 @@ # built documents. # # The short X.Y version. -version = '8.6.0' +version = '8.6.1' # The full version, including alpha/beta/rc tags. -release = '8.6.0' +release = '8.6.1' load_dotenv(override=True) DOC_VERSION = os.environ.get('DOC_VERSION', version)
docs/index.rst+7 −1 modified@@ -4,7 +4,7 @@ contain the root `toctree` directive. -DeepDiff 8.6.0 documentation! +DeepDiff 8.6.1 documentation! ============================= ******* @@ -31,6 +31,12 @@ The DeepDiff library includes the following modules: What Is New *********** +DeepDiff 8-6-1 +-------------- + + - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + + DeepDiff 8-6-0 --------------
pyproject.toml+1 −1 modified@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "deepdiff" -version = "8.6.0" +version = "8.6.1" dependencies = [ "orderly-set>=5.4.1,<6", ]
README.md+5 −2 modified@@ -1,4 +1,4 @@ -# DeepDiff v 8.6.0 +# DeepDiff v 8.6.1   @@ -17,12 +17,15 @@ Tested on Python 3.9+ and PyPy3. -- **[Documentation](https://zepworks.com/deepdiff/8.6.0/)** +- **[Documentation](https://zepworks.com/deepdiff/8.6.1/)** ## What is new? Please check the [ChangeLog](CHANGELOG.md) file for the detailed information. +DeepDiff 8-6-1 +- Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + DeepDiff 8-6-0 - Added Colored View thanks to @mauvilsa
tests/test_security.py+133 −0 added@@ -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
uv.lock+2 −2 modified@@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.12'", @@ -273,7 +273,7 @@ wheels = [ [[package]] name = "deepdiff" -version = "8.5.0" +version = "8.6.1" source = { editable = "." } dependencies = [ { name = "orderly-set" },
60ac5b903dbdMerge commit from fork
16 files changed · +178 −14
AUTHORS.md+1 −0 modified@@ -75,3 +75,4 @@ Authors in order of the timeline of their contributions: - [dtorres-sf](https://github.com/dtorres-sf) for the fix for moving nested tables when using iterable_compare_func. - [Jim Cipar](https://github.com/jcipar) for the fix recursion depth limit when hashing numpy.datetime64 - [Enji Cooper](https://github.com/ngie-eign) for converting legacy setuptools use to pyproject.toml +- [Diogo Correia](https://github.com/diogotcorreia) for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution.
.bumpversion.cfg+1 −1 modified@@ -1,5 +1,5 @@ [bumpversion] -current_version = 8.6.0 +current_version = 8.6.1 commit = True tag = True tag_name = {new_version}
CHANGELOG.md+4 −0 modified@@ -1,5 +1,9 @@ # DeepDiff Change log +- v8-6-1 + - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + + - v8-6-0 - Added Colored View thanks to @mauvilsa - Added support for applying deltas to NamedTuple thanks to @paulsc
CITATION.cff+1 −1 modified@@ -5,6 +5,6 @@ authors: given-names: "Sep" orcid: "https://orcid.org/0009-0009-5828-4345" title: "DeepDiff" -version: 8.6.0 +version: 8.6.1 date-released: 2024 url: "https://github.com/seperman/deepdiff"
deepdiff/delta.py+7 −1 modified@@ -17,7 +17,7 @@ ) 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 from deepdiff.summarize import summarize @@ -237,6 +237,11 @@ def _get_elem_and_compare_to_old_value( 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: @@ -536,6 +541,7 @@ def _get_elements_and_details(self, path): obj = self # obj = self.get_nested_obj(obj=self, elements=elements[:-1]) elem, action = elements[-1] # type: ignore + check_elem(elem) except Exception as e: self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e)) return None
deepdiff/__init__.py+1 −1 modified@@ -1,6 +1,6 @@ """This module offers the DeepDiff, DeepSearch, grep, Delta and DeepHash classes.""" # flake8: noqa -__version__ = '8.6.0' +__version__ = '8.6.1' import logging if __name__ == '__main__':
deepdiff/path.py+7 −0 modified@@ -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:
deepdiff/serialization.py+2 −2 modified@@ -59,7 +59,7 @@ class UnsupportedFormatErr(TypeError): 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', @@ -95,7 +95,7 @@ class UnsupportedFormatErr(TypeError): 'ipaddress.IPv4Address', 'ipaddress.IPv6Address', 'collections.abc.KeysView', -} +}) TYPE_STR_TO_TYPE = {
docs/authors.rst+1 −0 modified@@ -117,6 +117,7 @@ and polars support. limit when hashing numpy.datetime64 - `Enji Cooper <https://github.com/ngie-eign>`__ for converting legacy setuptools use to pyproject.toml +- `Diogo Correia <https://github.com/diogotcorreia>`__ for reporting security vulnerability in Delta and DeepDiff that could allow remote code execution. .. _Sep Dehpour (Seperman): http://www.zepworks.com
docs/changelog.rst+3 −0 modified@@ -5,6 +5,9 @@ Changelog DeepDiff Changelog +- v8-6-1 + - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + - v8-6-0 - Added Colored View thanks to @mauvilsa - Added support for applying deltas to NamedTuple thanks to @paulsc
docs/conf.py+2 −2 modified@@ -64,9 +64,9 @@ # built documents. # # The short X.Y version. -version = '8.6.0' +version = '8.6.1' # The full version, including alpha/beta/rc tags. -release = '8.6.0' +release = '8.6.1' load_dotenv(override=True) DOC_VERSION = os.environ.get('DOC_VERSION', version)
docs/index.rst+7 −1 modified@@ -4,7 +4,7 @@ contain the root `toctree` directive. -DeepDiff 8.6.0 documentation! +DeepDiff 8.6.1 documentation! ============================= ******* @@ -31,6 +31,12 @@ The DeepDiff library includes the following modules: What Is New *********** +DeepDiff 8-6-1 +-------------- + + - Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + + DeepDiff 8-6-0 --------------
pyproject.toml+1 −1 modified@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "deepdiff" -version = "8.6.0" +version = "8.6.1" dependencies = [ "orderly-set>=5.4.1,<6", ]
README.md+5 −2 modified@@ -1,4 +1,4 @@ -# DeepDiff v 8.6.0 +# DeepDiff v 8.6.1   @@ -17,12 +17,15 @@ Tested on Python 3.9+ and PyPy3. -- **[Documentation](https://zepworks.com/deepdiff/8.6.0/)** +- **[Documentation](https://zepworks.com/deepdiff/8.6.1/)** ## What is new? Please check the [ChangeLog](CHANGELOG.md) file for the detailed information. +DeepDiff 8-6-1 +- Patched security vulnerability in the Delta class which was vulnerable to class pollution via its constructor, and when combined with a gadget available in DeltaDiff itself, it could lead to Denial of Service and Remote Code Execution (via insecure Pickle deserialization). + DeepDiff 8-6-0 - Added Colored View thanks to @mauvilsa
tests/test_security.py+133 −0 added@@ -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
uv.lock+2 −2 modified@@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.12'", @@ -273,7 +273,7 @@ wheels = [ [[package]] name = "deepdiff" -version = "8.5.0" +version = "8.6.1" source = { editable = "." } dependencies = [ { name = "orderly-set" },
c69c06c13f75Security fix: Prevent class pollution and remote code execution in Delta
4 files changed · +149 −3
deepdiff/delta.py+7 −1 modified@@ -17,7 +17,7 @@ ) 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 from deepdiff.summarize import summarize @@ -237,6 +237,11 @@ def _get_elem_and_compare_to_old_value( 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: @@ -536,6 +541,7 @@ def _get_elements_and_details(self, path): obj = self # obj = self.get_nested_obj(obj=self, elements=elements[:-1]) elem, action = elements[-1] # type: ignore + check_elem(elem) except Exception as e: self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e)) return None
deepdiff/path.py+7 −0 modified@@ -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:
deepdiff/serialization.py+2 −2 modified@@ -59,7 +59,7 @@ class UnsupportedFormatErr(TypeError): 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', @@ -95,7 +95,7 @@ class UnsupportedFormatErr(TypeError): 'ipaddress.IPv4Address', 'ipaddress.IPv6Address', 'collections.abc.KeysView', -} +}) TYPE_STR_TO_TYPE = {
tests/test_security.py+133 −0 added@@ -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
c69c06c13f75Security fix: Prevent class pollution and remote code execution in Delta
4 files changed · +149 −3
deepdiff/delta.py+7 −1 modified@@ -17,7 +17,7 @@ ) 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 from deepdiff.summarize import summarize @@ -237,6 +237,11 @@ def _get_elem_and_compare_to_old_value( 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: @@ -536,6 +541,7 @@ def _get_elements_and_details(self, path): obj = self # obj = self.get_nested_obj(obj=self, elements=elements[:-1]) elem, action = elements[-1] # type: ignore + check_elem(elem) except Exception as e: self._raise_or_log(UNABLE_TO_GET_ITEM_MSG.format(path, e)) return None
deepdiff/path.py+7 −0 modified@@ -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:
deepdiff/serialization.py+2 −2 modified@@ -59,7 +59,7 @@ class UnsupportedFormatErr(TypeError): 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', @@ -95,7 +95,7 @@ class UnsupportedFormatErr(TypeError): 'ipaddress.IPv4Address', 'ipaddress.IPv6Address', 'collections.abc.KeysView', -} +}) TYPE_STR_TO_TYPE = {
tests/test_security.py+133 −0 added@@ -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
2015f0a4bcdbfix: only prevent access to object paths containing __globals__ and __builtins__ for non-dict/list objects
3 files changed · +28 −10
CHANGELOG.rst+3 −0 modified@@ -3,6 +3,9 @@ Changelog ========= +- Only prevent access to object paths containing ``__globals__`` or ``__builtins__`` instead of all dunder-methods for non-dict/list objects. + + v6.0.1 (2023-02-20) -------------------
src/pydash/helpers.py+5 −5 modified@@ -20,6 +20,9 @@ #: Dictionary of builtins with keys as the builtin function and values as the string name. BUILTINS = {value: key for key, value in builtins.__dict__.items() if isinstance(value, Hashable)} +#: Object keys that are restricted from access via path access. +RESTRICTED_KEYS = ("__globals__", "__builtins__") + def callit(iteratee, *args, **kwargs): """Inspect argspec of `iteratee` function and only pass the supported arguments when calling @@ -188,11 +191,8 @@ def _base_get_object(obj, key, default=UNSET): def _raise_if_restricted_key(key): - if not isinstance(key, str): - return - # Prevent access to dunder-methods since this could expose access to globals through leaky - # attributes such as obj.__init__.__globals__. - if len(key) > 4 and key.isascii() and key.startswith("__") and key.endswith("__"): + # Prevent access to restricted keys for security reasons. + if key in RESTRICTED_KEYS: raise KeyError(f"access to restricted key {key!r} is not allowed")
tests/test_objects.py+20 −5 modified@@ -384,9 +384,10 @@ def test_get__should_not_populate_defaultdict(): @parametrize( "obj,path", [ - (helpers.Object(), "__init__"), - (helpers.Object(subobj=helpers.Object()), "subobj.__init__"), - (namedtuple("a", ["a"])(a=1), "__len__"), + (helpers.Object(), "__init__.__globals__"), + (namedtuple("a", ["a"])(a=1), "__globals__"), + (helpers.Object(subobj=helpers.Object()), "subobj.__builtins__"), + (helpers.Object(subobj=helpers.Object()), "__builtins__"), ], ) def test_get__raises_for_objects_when_path_restricted(obj, path): @@ -397,14 +398,28 @@ def test_get__raises_for_objects_when_path_restricted(obj, path): @parametrize( "obj,path", [ - ({}, "__init__"), - ([], "__contains__"), + ({}, "__globals__"), + ({}, "__builtins__"), + ([], "__globals__"), + ([], "__builtins__"), ], ) def test_get__does_not_raise_for_dict_or_list_when_path_restricted(obj, path): assert _.get(obj, path) is None +@parametrize( + "obj,path", + [ + (helpers.Object(), "__name__"), + (helpers.Object(), "foo.__dict__"), + (helpers.Object(), "__len__"), + ], +) +def test_get__does_not_raise_for_objects_when_path_is_unrestricted(obj, path): + assert _.get(obj, path) is None + + @parametrize( "case,expected", [
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-mw26-5g2v-hqw3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-58367ghsaADVISORY
- github.com/dgilland/pydash/commit/2015f0a4bcdbc3a5b27652e38fe97b3ee13ac15fghsaWEB
- github.com/dgilland/pydash/issues/180ghsaWEB
- github.com/seperman/deepdiff/commit/c69c06c13f75e849c770ade3f556cd16209fd183nvdWEB
- github.com/seperman/deepdiff/releases/tag/8.6.1nvdWEB
- github.com/seperman/deepdiff/security/advisories/GHSA-mw26-5g2v-hqw3nvdWEB
News mentions
0No linked articles in our index yet.