From 6d9b9945fce684b846c47ea194a5f8b4447bebd5 Mon Sep 17 00:00:00 2001 From: Khalil Ben Safta Date: Wed, 15 Jul 2026 21:11:06 +0200 Subject: [PATCH 1/2] Harden JSON deserialization against arbitrary code execution --- src/pandapipes/io/file_io.py | 14 ++++++++++---- src/pandapipes/io/io_utils.py | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pandapipes/io/file_io.py b/src/pandapipes/io/file_io.py index 28821325d..d8f13032c 100644 --- a/src/pandapipes/io/file_io.py +++ b/src/pandapipes/io/file_io.py @@ -108,7 +108,7 @@ def from_pickle(filename): return net -def from_json(filename, convert=True, encryption_key=None, ignore_unknown_objects=False): +def from_json(filename, convert=True, encryption_key=None, ignore_unknown_objects=False, skip_checks=False): """ Load a pandapipes network from a JSON file or string. The index of the returned network is not necessarily in the same order as the original network. @@ -123,6 +123,8 @@ def from_json(filename, convert=True, encryption_key=None, ignore_unknown_object :param ignore_unknown_objects: if set to True, ignore any objects that cannot be deserialized \ instead of raising an error :type ignore_unknown_objects: bool + :param skip_checks: If set to True, no security checks will be performed when deserializing. + :type skip_checks: bool :return: net - The pandapipes network that was saved as JSON :rtype: pandapipesNet @@ -139,10 +141,11 @@ def from_json(filename, convert=True, encryption_key=None, ignore_unknown_object with open(filename) as fp: json_string = fp.read() return from_json_string(json_string, convert=convert, encryption_key=encryption_key, - ignore_unknown_objects=ignore_unknown_objects) + ignore_unknown_objects=ignore_unknown_objects, + skip_checks=skip_checks) -def from_json_string(json_string, convert=False, encryption_key=None, ignore_unknown_objects=False): +def from_json_string(json_string, convert=False, encryption_key=None, ignore_unknown_objects=False, skip_checks=False): """ Load a pandapipes network from a JSON string. The index of the returned network is not necessarily in the same order as the original network. @@ -157,6 +160,8 @@ def from_json_string(json_string, convert=False, encryption_key=None, ignore_unk :param ignore_unknown_objects: if set to True, ignore any objects that cannot be deserialized \ instead of raising an error :type ignore_unknown_objects: bool + :param skip_checks: If set to True, no security checks will be performed when deserializing. + :type skip_checks: bool :return: net - The pandapipes network that was contained in the JSON string :rtype: pandapipesNet @@ -169,7 +174,8 @@ def from_json_string(json_string, convert=False, encryption_key=None, ignore_unk json_string = decrypt_string(json_string, encryption_key) net = json.loads(json_string, cls=PPJSONDecoder, registry_class=FromSerializableRegistryPpipe, - ignore_unknown_objects=ignore_unknown_objects) + ignore_unknown_objects=ignore_unknown_objects, + skip_checks=skip_checks) if convert and isinstance(net, pandapipesNet): convert_format(net) diff --git a/src/pandapipes/io/io_utils.py b/src/pandapipes/io/io_utils.py index 233a3b7d6..370202450 100644 --- a/src/pandapipes/io/io_utils.py +++ b/src/pandapipes/io/io_utils.py @@ -43,7 +43,7 @@ class FromSerializableRegistryPpipe(FromSerializableRegistry): module_name = '' omit_modules = '' - def __init__(self, obj, d, ppipes_hook, ignore_unknown_objects=False, omit_modules=None): + def __init__(self, obj, d, ppipes_hook, ignore_unknown_objects=False, omit_modules=None, **kwargs): """ :param obj: object the data is written to @@ -53,7 +53,7 @@ def __init__(self, obj, d, ppipes_hook, ignore_unknown_objects=False, omit_modul :param ppipes_hook: a way how to handle non-default data :type ppipes_hook: funct """ - super().__init__(obj, d, ppipes_hook, ignore_unknown_objects, omit_modules) + super().__init__(obj, d, ppipes_hook, ignore_unknown_objects, omit_modules, **kwargs) @from_serializable.register(class_name="method") def method(self): @@ -101,7 +101,8 @@ def rest(self): self.obj, cls=PPJSONDecoder, object_hook=partial(pp_hook, registry_class=FromSerializableRegistryPpipe, ignore_unknown_objects=self.ignore_unknown_objects, - omit_modules=self.omit_modules) + omit_modules=self.omit_modules, + skip_checks=getattr(self, "skip_checks", False)) ) # backwards compatibility if "net" in self.obj: @@ -111,6 +112,14 @@ def rest(self): return class_ else: # for non-pp objects, e.g. tuple + skip_checks = getattr(self, "skip_checks", False) + if not skip_checks: + try: + from pandapower.io_utils import _is_safe_to_deserialize, DeserializationNotAllowed + if not _is_safe_to_deserialize(self.module_name, self.class_name, class_): + raise DeserializationNotAllowed(f"Deserializing '{self.module_name}.{self.class_name}' is not allowed in pandapipes") + except ImportError: + pass # Fallback for old pandapower versions without the security patch return class_(self.obj, **self.d) @from_serializable.register(class_name='MultiNet') From 626cee8bcc29790442c7078c75e5c87f9df86d34 Mon Sep 17 00:00:00 2001 From: Khalil Ben Safta Date: Fri, 17 Jul 2026 14:21:10 +0200 Subject: [PATCH 2/2] Bumped pandapower version from 3.3.3 to 5.4.3, ie last stable release. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 80cdeba90..487288b67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ "Programming Language :: Python :: 3.12" ] dependencies = [ - "pandapower == 3.3.3", + "pandapower == 3.5.4", "matplotlib", "shapely", ]