VAITP Dataset

← Back to the dataset

CVE-2026-40288

PraisonAI workflow engine executes arbitrary code from untrusted YAML files.

  • CVSS 9.8
  • CWE-78
  • Input Validation and Sanitization
  • Local

PraisonAI is a multi-agent teams system. In versions below 4.5.139 of PraisonAI and 1.5.140 of praisonaiagents, the workflow engine is vulnerable to arbitrary command and code execution through untrusted YAML files. When praisonai workflow run <file.yaml> loads a YAML file with type: job, the JobWorkflowExecutor in job_workflow.py processes steps that support run: (shell commands via subprocess.run()), script: (inline Python via exec()), and python: (arbitrary Python script execution)—all without any validation, sandboxing, or user confirmation. The affected code paths include action_run() in workflow.py and _exec_shell(), _exec_inline_python(), and _exec_python_script() in job_workflow.py. An attacker who can supply or influence a workflow YAML file (particularly in CI pipelines, shared repositories, or multi-tenant deployment environments) can achieve full arbitrary command execution on the host system, compromising the machine and any accessible data or credentials. This issue has been fixed in versions 4.5.139 of PraisonAI and 1.5.140 of praisonaiagents.

CVSS base score
9.8
Published
2026-04-14
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Command Injection
Accessibility scope
Local
Impact
Arbitrary Code Execution
Affected component
PraisonAI an
Fixed by upgrading
Yes

Solution

Upgrade PraisonAI to version 4.5.139 or later and praisonaiagents to version 1.5.140 or later.

Vulnerable code sample

#
# This code is a representation of the vulnerable logic described in CVE-2026-40288
# for PraisonAI versions prior to 4.5.139 and praisonaiagents prior to 1.5.140.
# It is intended for educational and demonstration purposes only.
#
# To run this demonstration:
# 1. Save this file as `vulnerable_executor.py`.
# 2. Install the PyYAML library: pip install PyYAML
# 3. Create a YAML file named `malicious_workflow.yaml` with the following content:
#
#    type: job
#    name: Malicious CI Job
#    steps:
#      - name: "Execute shell command"
#        run: "echo 'Arbitrary command execution via run:' && whoami && id"
#
#      - name: "Execute inline python script"
#        script: |
#          import os
#          print("\nArbitrary code execution via inline script:")
#          print(f"Current PID: {os.getpid()}")
#
#      - name: "Execute python script from file"
#        python: "malicious_script.py"
#
# 4. Create a Python file named `malicious_script.py` with the following content:
#
#    import platform
#    print("\nArbitrary code execution via python script file:")
#    print(f"System Information: {platform.uname()}")
#
# 5. Run the vulnerable executor from your terminal:
#    python vulnerable_executor.py malicious_workflow.yaml
#
# This will execute the commands and code defined in the YAML file without any
# validation, sandboxing, or user confirmation, demonstrating the vulnerability.

import yaml
import subprocess
import sys
import os

class JobWorkflowExecutor:
    """
    A simplified representation of the vulnerable JobWorkflowExecutor in job_workflow.py.
    """
    def __init__(self, workflow_data):
        self.workflow_data = workflow_data
        self.steps = self.workflow_data.get('steps', [])

    def execute(self):
        """Processes each step in the workflow."""
        print(f"--- Starting execution of workflow: {self.workflow_data.get('name', 'Untitled')} ---")
        for i, step in enumerate(self.steps):
            print(f"\n[Step {i+1}/{len(self.steps)}] Executing: {step.get('name', 'Unnamed Step')}")
            if 'run' in step:
                self._exec_shell(step['run'])
            elif 'script' in step:
                self._exec_inline_python(step['script'])
            elif 'python' in step:
                self._exec_python_script(step['python'])
            else:
                print("Warning: Step does not contain a runnable action ('run', 'script', 'python').")
        print("\n--- Workflow execution finished ---")

    def _exec_shell(self, command: str):
        """
        VULNERABILITY: Executes a shell command directly using subprocess.run
        with shell=True, allowing for command injection. This represents the
        vulnerability in the _exec_shell() function.
        """
        try:
            # The use of shell=True on untrusted input is the vulnerability.
            subprocess.run(command, shell=True, check=True, text=True)
        except subprocess.CalledProcessError as e:
            print(f"Error executing shell command: {e}")
        except FileNotFoundError as e:
            print(f"Command not found: {e}")

    def _exec_inline_python(self, code: str):
        """
        VULNERABILITY: Executes an inline string of Python code using exec().
        This represents the vulnerability in the _exec_inline_python() function.
        """
        try:
            # The use of exec() on untrusted code from the YAML is the vulnerability.
            exec(code)
        except Exception as e:
            print(f"Error executing inline Python script: {e}")

    def _exec_python_script(self, script_path: str):
        """
        VULNERABILITY: Reads and executes a Python script from an arbitrary file path.
        This represents the vulnerability in the _exec_python_script() function.
        """
        if not os.path.exists(script_path):
            print(f"Error: Python script file not found at '{script_path}'")
            return
        
        try:
            with open(script_path, 'r') as f:
                script_content = f.read()
            # The use of exec() on untrusted code from a file is the vulnerability.
            exec(script_content)
        except Exception as e:
            print(f"Error executing Python script from file '{script_path}': {e}")


def action_run(file_path: str):
    """
    A simplified representation of the action_run() function in workflow.py
    which loads the YAML and triggers the execution.
    """
    if not os.path.exists(file_path):
        print(f"Error: Workflow file not found at '{file_path}'")
        return

    try:
        with open(file_path, 'r') as f:
            # In a real scenario, this might use yaml.load() which is even more dangerous.
            # However, the described vulnerability lies in the execution, not the parsing.
            workflow_data = yaml.safe_load(f)
    except yaml.YAMLError as e:
        print(f"Error parsing YAML file: {e}")
        return

    # Check if the workflow is of type 'job', as described in the CVE.
    if workflow_data.get('type') == 'job':
        executor = JobWorkflowExecutor(workflow_data)
        executor.execute()
    else:
        print(f"Workflow type is not 'job'. Skipping execution.")


if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python vulnerable_executor.py <path_to_workflow.yaml>")
        sys.exit(1)

    workflow_file = sys.argv[1]
    
    # This call simulates `praisonai workflow run <file.yaml>`
    action_run(workflow_file)

Patched code sample

import subprocess
import sys
import logging

# Configure basic logging for clarity in execution
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class JobWorkflowExecutor:
    """
    Executes jobs defined in a workflow YAML file.
    This version includes security fixes for CVE-2026-40288 by requiring
    user confirmation before executing shell commands or Python code.
    """

    def __init__(self, interactive_mode=True):
        """
        Initializes the executor.

        Args:
            interactive_mode (bool): If False, disables security confirmations and
                                     refuses to run unsafe steps. This is for
                                     non-interactive CI/CD environments where safety
                                     is paramount.
        """
        self.interactive_mode = interactive_mode
        if not self.interactive_mode:
            logging.warning(
                "NON-INTERACTIVE MODE: Execution of 'run', 'script', and 'python' steps is disabled for security."
            )

    # --- START OF FIX for CVE-2026-40288 ---

    def _confirm_execution(self, execution_type: str, content: str) -> bool:
        """
        Prompts the user for confirmation before executing a potentially unsafe command or script.
        This function is the core of the fix, addressing the lack of validation or user confirmation.
        """
        if not self.interactive_mode:
            logging.error(
                f"Attempted to execute an unsafe step ('{execution_type}') in a non-interactive environment. "
                "Execution denied for security reasons."
            )
            return False

        print("-" * 60)
        print("⚠️  SECURITY WARNING: The workflow is attempting to execute a local task.")
        print(f"   Type: {execution_type}")
        print("   Content:")
        # Indent content for readability
        for line in content.strip().splitlines():
            print(f"     {line}")
        print("-" * 60)

        try:
            response = input("Do you want to allow this execution? [y/N]: ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            # Handle non-interactive environments or user cancellation
            response = 'n'
            print("\nConfirmation cancelled or input stream closed. Denying execution.")

        if response in ('y', 'yes'):
            logging.info("User approved execution.")
            return True
        else:
            logging.error("User denied execution. Aborting workflow.")
            return False

    # --- END OF FIX ---

    def _exec_shell(self, command: str):
        """
        Executes a shell command only after receiving explicit user confirmation.
        """
        # FIX: The direct call to subprocess is now gated by the confirmation function.
        if self._confirm_execution("Shell Command (`run:`)", command):
            try:
                logging.info(f"Executing approved shell command: {command}")
                # The original vulnerability was calling this directly.
                subprocess.run(command, shell=True, check=True, text=True)
                logging.info("Command executed successfully.")
            except subprocess.CalledProcessError as e:
                logging.error(f"Command failed with exit code {e.returncode}")
                sys.exit(1)
            except Exception as e:
                logging.error(f"An unexpected error occurred during shell execution: {e}")
                sys.exit(1)
        else:
            sys.exit(1) # Abort workflow if user denies

    def _exec_inline_python(self, script: str):
        """
        Executes an inline Python script only after receiving explicit user confirmation.
        """
        # FIX: The direct call to exec() is now gated by the confirmation function.
        if self._confirm_execution("Inline Python Script (`script:`)", script):
            try:
                logging.info("Executing approved inline Python script.")
                # The original vulnerability was calling exec() on untrusted input directly.
                exec(script, {'__builtins__': __builtins__})
                logging.info("Inline script executed successfully.")
            except Exception as e:
                logging.error(f"Inline Python script failed: {e}")
                sys.exit(1)
        else:
            sys.exit(1) # Abort workflow if user denies

    def _exec_python_script(self, script_path: str):
        """
        Reads and executes a Python script from a file only after user confirmation.
        """
        try:
            with open(script_path, 'r') as f:
                script_content = f.read()
        except FileNotFoundError:
            logging.error(f"Python script not found at path: {script_path}")
            sys.exit(1)
        except Exception as e:
            logging.error(f"Failed to read script file '{script_path}': {e}")
            sys.exit(1)

        # FIX: Execution of the script's content is gated by user confirmation.
        if self._confirm_execution(f"Python Script File (`python: {script_path}`)", script_content):
            try:
                logging.info(f"Executing approved Python script: {script_path}")
                # The original vulnerability was executing this content without checks.
                exec(script_content, {'__builtins__': __builtins__})
                logging.info(f"Script '{script_path}' executed successfully.")
            except Exception as e:
                logging.error(f"Execution of script '{script_path}' failed: {e}")
                sys.exit(1)
        else:
            sys.exit(1) # Abort workflow if user denies

    def execute_step(self, step: dict):
        """
        Parses and executes a single step from the workflow, dispatching to
        the newly secured execution methods.
        """
        if "run" in step:
            self._exec_shell(step["run"])
        elif "script" in step:
            self._exec_inline_python(step["script"])
        elif "python" in step:
            self._exec_python_script(step["python"])
        else:
            logging.info(f"Step '{step.get('name', 'Unnamed')}' contains no executable action.")

Payload

type: job
steps:
  - name: Exploit
    run: "touch /tmp/pwned"

Cite this entry

@misc{vaitp:cve202640288,
  title        = {{PraisonAI workflow engine executes arbitrary code from untrusted YAML files.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-40288},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40288/}}
}
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 ::