GHSA-26mh-57q7-jfvr

    Dashboard / Vulnerabilities / GHSA-26mh-57q7-jfvr

    GHSA-26mh-57q7-jfvr

    Published: 29 Oct 2026Last Modified: 29 Oct 2026

    Summary: PraisonAI vulnerable to Remote Code Execution via Broken AST Sandbox

    Details: ### Summary JobWorkflowExecutor._exec_inline_python() is designed to safely execute inline Python expressions from YAML workflow script: steps using AST validation. The function claims to block dangerous operations by (a) checking all AST nodes against an allowlist, and (b) providing a __builtins__ whitelist that omits __import__, exec, eval, and open. The sandbox is broken. The module import allowlist (allowed_modules) explicitly includes 'os', and the AST allowlist permits ast.Import, ast.ImportFrom, ast.Call, and ast.Attribute nodes. This means the following malicious workflow script: step passes all AST validation checks and executes without error. Attack Scenario Attacker creates or contributes a YAML workflow file (type: job) with a script: step containing import os; os.system(...). A user or CI pipeline with PRAISONAI_ALLOW_JOB_WORKFLOWS=true executes the workflow via praisonai workflow run malicious.yaml. Arbitrary OS commands execute with the privileges of the calling process. This is particularly dangerous in multi-tenant or CI environments where YAML workflows may originate from untrusted sources (e.g., downloaded templates, shared repositories, AI-generated workflows). ### PoC _Complete instructions, including specific configuration details, to reproduce the vulnerability._ ### Impact Root Cause — Lines 364–372: pythonallowed_modules = {'os', 'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools'} if isinstance(node, ast.Import): for alias in node.names: if alias.name.split('.')[0] not in allowed_modules: return {"ok": False, "error": f"Import of '{alias.name}' not allowed..."} os is trusted because it is commonly used for path operations (os.path, os.getcwd). However, os.system(), os.popen(), os.execvp(), os.spawn*(), and os._exit() are all accessible once the module is imported, none of which are individually blocked. The __builtins__ restriction (blocking __import__) is irrelevant because the code uses a real import statement processed by the Python bytecode compiler — not a __import__() call. Proof-of-Concept validation (static — no remote network call made): pythonimport ast _ALLOWED_NODES = ( ast.Expression, ast.BoolOp, ast.BinOp, ast.UnaryOp, ast.Compare, ast.IfExp, ast.Constant, ast.Name, ast.Load, ast.Store, ast.Subscript, ast.List, ast.Tuple, ast.Dict, ast.Set, ast.And, ast.Or, ast.Not, ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.FloorDiv, ast.USub, ast.UAdd, ast.Call, ast.Attribute, ast.keyword, ast.Starred, ast.Module, ast.Assign, ast.Import, ast.ImportFrom, ast.alias, ) allowed_modules = {'os', 'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools'} malicious = "import os\nresult = os.system('id')" tree = ast.parse(malicious, mode="exec") all_pass = True for node in ast.walk(tree): if not isinstance(node, _ALLOWED_NODES): print(f"BLOCKED: {type(node).__name__}") all_pass = False if isinstance(node, ast.Import): for alias in node.names: if alias.name.split('.')[0] not in allowed_modules: print(f"IMPORT BLOCKED: {alias.name}") all_pass = False print("SANDBOX BYPASSED" if all_pass else "sandbox holds") # Output: SANDBOX BYPASSED Recommended Fix Remove 'os' from allowed_modules and replace it with only the safe submodule: 'os.path'. Then validate that the imported name is specifically os.path, not os: python# BEFORE (vulnerable) allowed_modules = {'os', 'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools'} # AFTER (safe) allowed_modules = {'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools', 'pathlib'} # If os.path is needed, handle it explicitly: if isinstance(node, ast.ImportFrom): if node.module == 'os.path': pass # allow 'from os.path import join, exists' only elif node.module and node.module.split('.')[0] not in allowed_modules: return {"ok": False, "error": f"Import of '{node.module}' not allowed."} Additionally, block ast.Import of os entirely, and consider also removing re (which can be used for ReDoS/CPU exhaustion) and collections (which can be used to reach __subclasses__-style gadgets).

    Affected packages

    Package

    Name: praisonai

    Purl: pkg:pypi/praisonai

    Affected ranges

    Type: ECOSYSTEM

    Events:

    Introduced- 0
    Fixed -4.6.78

    Affected versions

    0.0.1
    0.0.10
    0.0.11
    0.0.12
    0.0.13
    0.0.14
    0.0.15
    0.0.16
    0.0.17
    0.0.18
    0.0.19
    0.0.2
    0.0.20
    0.0.21
    0.0.22
    0.0.23
    0.0.24
    0.0.25
    0.0.26
    0.0.27
    0.0.28
    0.0.29
    0.0.3
    0.0.30
    0.0.31
    0.0.32
    0.0.33
    0.0.34
    0.0.35
    0.0.36
    0.0.37
    0.0.38
    0.0.39
    0.0.4
    0.0.40
    0.0.41
    0.0.42
    0.0.43
    0.0.44
    0.0.45
    0.0.46
    0.0.47
    0.0.48
    0.0.49
    0.0.5
    0.0.50
    0.0.52
    0.0.53
    0.0.54
    0.0.55
    0.0.56
    0.0.57
    0.0.58
    0.0.59
    0.0.59rc11
    0.0.59rc2
    0.0.59rc3
    0.0.59rc5
    0.0.59rc6
    0.0.59rc7
    0.0.59rc8
    0.0.59rc9
    0.0.6
    0.0.61
    0.0.64
    0.0.65
    0.0.66
    0.0.67
    0.0.68
    0.0.69
    0.0.7
    0.0.70
    0.0.71
    0.0.72
    0.0.73
    0.0.74
    0.0.8
    0.0.9

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High