Skip to content

PraisonAI vulnerable to Remote Code Execution via Broken AST Sandbox

High severity GitHub Reviewed Published Jun 25, 2026 in MervinPraison/PraisonAI • Updated Oct 29, 2026

Package

pip praisonai (pip)

Affected versions

< 4.6.78

Patched versions

4.6.78

Description

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).

References

@MervinPraison MervinPraison published to MervinPraison/PraisonAI Jun 25, 2026
Published by the National Vulnerability Database Jul 15, 2026
Published to the GitHub Advisory Database Oct 29, 2026
Reviewed Oct 29, 2026
Last updated Oct 29, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
Low
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(24th percentile)

Weaknesses

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component. Learn more on MITRE.

Incomplete List of Disallowed Inputs

The product implements a protection mechanism that relies on a list of inputs (or properties of inputs) that are not allowed by policy or otherwise require other action to neutralize before additional processing takes place, but the list is incomplete. Learn more on MITRE.

Protection Mechanism Failure

The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product. Learn more on MITRE.

CVE ID

CVE-2026-61438

GHSA ID

GHSA-26mh-57q7-jfvr

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.