PT-2026-133261
CVE-2026-62248
·
Published
2026-10-29
·
Updated
2026-10-29
Description by
CVSS 3.1
7.3
HighMetricsAV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H
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
result = 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).
Found an issue in the description? Have something to add? Feel free to write us 👾