VYPR
Critical severityNVD Advisory· Published Feb 6, 2026· Updated Feb 6, 2026

EPyT-Flow has unsafe JSON deserialization (__type__)

CVE-2026-25632

Description

EPyT-Flow is a Python package designed for the easy generation of hydraulic and water quality scenario data of water distribution networks. Prior to 0.16.1, EPyT-Flow’s REST API parses attacker-controlled JSON request bodies using a custom deserializer (my_load_from_json) that supports a type field. When type is present, the deserializer dynamically imports an attacker-specified module/class and instantiates it with attacker-supplied arguments. This allows invoking dangerous classes such as subprocess.Popen, which can lead to OS command execution during JSON parsing. This also affects the loading of JSON files. This vulnerability is fixed in 0.16.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

EPyT-Flow before 0.16.1 allows remote code execution via a custom JSON deserializer that dynamically imports attacker-controlled classes like subprocess.Popen.

Vulnerability

Overview

CVE-2026-25632 is a critical deserialization vulnerability in EPyT-Flow, a Python package for water distribution network simulation. The package's REST API and JSON file loading functionality use a custom deserializer (my_load_from_json) that supports a type field in JSON payloads. When present, the deserializer dynamically imports an attacker-specified Python module and class, then instantiates it with attacker-supplied arguments [1]. This design bypasses standard Python deserialization safety checks, allowing invocation of arbitrary classes such as subprocess.Popen to be invoked directly during JSON parsing.

Exploitation

An attacker can exploit this vulnerability by sending a crafted JSON request to the REST API or by providing a malicious JSON file for loading. No authentication is required if the REST API is exposed. The attacker controls both the module/class name and the constructor arguments, enabling arbitrary class instantiation. The commit that fixes the issue shows that the previous code used importlib.import_module and getattr to resolve and call any class, with no allowlist [3].

Impact

Successful exploitation leads to OS command execution in the context of the EPyT-Flow process. This could allow an attacker to execute arbitrary commands, potentially compromising the host system, accessing sensitive data, or disrupting water distribution network simulations. The vulnerability is rated with a high CVSS score [1].

Mitigation

The vulnerability is fixed in version 0.16.1. The fix replaces the unrestricted dynamic import with a whitelist of allowed serializable classes (JSON_SERIALIZABLE), preventing instantiation of arbitrary types [3]. Users should upgrade immediately. No workaround is available for versions prior to 0.16.1 [4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
epyt-flowPyPI
< 0.16.10.16.1

Affected products

1
  • WaterFutures/EPyT-Flowv5
    Range: < 0.16.1

Patches

1
3fff9151494c

Add sanity checks to JSON deserialization

https://github.com/WaterFutures/EPyT-FlowAndré ArteltFeb 4, 2026via ghsa
1 file changed · +23 13
  • epyt_flow/serialization.py+23 13 modified
    @@ -5,7 +5,6 @@
     from abc import abstractmethod, ABC
     from io import BufferedIOBase
     import pathlib
    -import importlib
     import json
     import gzip
     import umsgpack
    @@ -55,6 +54,10 @@
     COLOR_SCHEMES_ID                        = 36
     
     
    +JSON_SERIALIZABLE = {
    +}
    +
    +
     def my_packb(data: Any) -> bytes:
         """
         Overriden `umsgpack.packb <https://msgpack-python.readthedocs.io/en/latest/api.html#msgpack.packb>`_
    @@ -86,6 +89,9 @@ def serializable(my_id: int, my_file_ext: str) -> Any:
             File extension.
         """
         def wrapper(my_class):
    +        if issubclass(my_class, JsonSerializable):
    +            JSON_SERIALIZABLE[(my_class.__module__, my_class.__name__)] = my_class
    +
             @staticmethod
             def unpackb(data: bytes) -> Any:
                 return my_class(**my_unpackb(data))
    @@ -274,18 +280,22 @@ def my_load_from_json(data: str) -> Any:
         `Any`
             Deserialized object.
         """
    -    def __object_hook(obj: dict) -> dict:
    -        if "__type__" in obj:
    -            module_name, class_name = obj["__type__"]
    -            cls = getattr(importlib.import_module(module_name), class_name)
    -            del obj["__type__"]
    -
    -            for attr in obj:
    -                if isinstance(attr, dict):
    -                    obj[attr] = __object_hook(obj[attr])
    -
    -            return cls(**obj)
    -        return obj
    +    def __object_hook(obj: dict):
    +        t = obj.get("__type__")
    +        if not t:
    +            return obj
    +
    +        if not (isinstance(t, (list, tuple)) and len(t) == 2 and
    +                all(isinstance(x, str) for x in t)):
    +            raise ValueError("Invalid __type__")
    +
    +        key = (t[0], t[1])
    +        cls = JSON_SERIALIZABLE.get(key)
    +        if cls is None:
    +            raise ValueError(f"Type not allowed: {key}")
    +
    +        args = {k: v for k, v in obj.items() if k != "__type__"}
    +        return cls(**args)
     
         return json.loads(data, object_hook=__object_hook)
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.