VAITP Dataset

← Back to the dataset

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.

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 None

Patched 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 None

Payload

# 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 ::