GHSA-26MH-57Q7-JFVR
Vulnerability from github – Published: 2026-10-29 16:34 – Updated: 2026-10-29 16:34Summary
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": [
{
"package": {
"ecosystem": "PyPI",
"name": "praisonai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.6.78"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-61438"
],
"database_specific": {
"cwe_ids": [
"CWE-184",
"CWE-693",
"CWE-78"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-29T16:34:26Z",
"nvd_published_at": "2026-07-15T17:16:52Z",
"severity": "HIGH"
},
"details": "### Summary\nJobWorkflowExecutor._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.\n\nThe sandbox is broken. The module import allowlist (allowed_modules) explicitly includes \u0027os\u0027, 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.\n\nAttack Scenario\n\nAttacker creates or contributes a YAML workflow file (type: job) with a script: step containing import os; os.system(...).\nA user or CI pipeline with PRAISONAI_ALLOW_JOB_WORKFLOWS=true executes the workflow via praisonai workflow run malicious.yaml.\nArbitrary OS commands execute with the privileges of the calling process.\n\n\nThis 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).\n### PoC\n_Complete instructions, including specific configuration details, to reproduce the vulnerability._\n\n### Impact\n\nRoot Cause \u2014 Lines 364\u2013372:\n\npythonallowed_modules = {\u0027os\u0027, \u0027datetime\u0027, \u0027time\u0027, \u0027json\u0027, \u0027math\u0027, \u0027re\u0027, \u0027collections\u0027, \u0027itertools\u0027}\nif isinstance(node, ast.Import):\n for alias in node.names:\n if alias.name.split(\u0027.\u0027)[0] not in allowed_modules:\n return {\"ok\": False, \"error\": f\"Import of \u0027{alias.name}\u0027 not allowed...\"}\n\nos 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.\n\nThe __builtins__ restriction (blocking __import__) is irrelevant because the code uses a real import statement processed by the Python bytecode compiler \u2014 not a __import__() call.\n\nProof-of-Concept validation (static \u2014 no remote network call made):\n\npythonimport ast\n\n_ALLOWED_NODES = (\n ast.Expression, ast.BoolOp, ast.BinOp, ast.UnaryOp, ast.Compare,\n ast.IfExp, ast.Constant, ast.Name, ast.Load, ast.Store, ast.Subscript,\n ast.List, ast.Tuple, ast.Dict, ast.Set,\n ast.And, ast.Or, ast.Not, ast.Eq, ast.NotEq, ast.Lt, ast.LtE,\n ast.Gt, ast.GtE, ast.In, ast.NotIn, ast.Add, ast.Sub, ast.Mult,\n ast.Div, ast.Mod, ast.FloorDiv, ast.USub, ast.UAdd,\n ast.Call, ast.Attribute, ast.keyword, ast.Starred,\n ast.Module, ast.Assign, ast.Import, ast.ImportFrom, ast.alias,\n)\nallowed_modules = {\u0027os\u0027, \u0027datetime\u0027, \u0027time\u0027, \u0027json\u0027, \u0027math\u0027, \u0027re\u0027, \u0027collections\u0027, \u0027itertools\u0027}\n\nmalicious = \"import os\\nresult = os.system(\u0027id\u0027)\"\ntree = ast.parse(malicious, mode=\"exec\")\n\nall_pass = True\nfor node in ast.walk(tree):\n if not isinstance(node, _ALLOWED_NODES):\n print(f\"BLOCKED: {type(node).__name__}\")\n all_pass = False\n if isinstance(node, ast.Import):\n for alias in node.names:\n if alias.name.split(\u0027.\u0027)[0] not in allowed_modules:\n print(f\"IMPORT BLOCKED: {alias.name}\")\n all_pass = False\n\nprint(\"SANDBOX BYPASSED\" if all_pass else \"sandbox holds\")\n# Output: SANDBOX BYPASSED\n\n\nRecommended Fix\n\nRemove \u0027os\u0027 from allowed_modules and replace it with only the safe submodule: \u0027os.path\u0027. Then validate that the imported name is specifically os.path, not os:\n\npython# BEFORE (vulnerable)\nallowed_modules = {\u0027os\u0027, \u0027datetime\u0027, \u0027time\u0027, \u0027json\u0027, \u0027math\u0027, \u0027re\u0027, \u0027collections\u0027, \u0027itertools\u0027}\n\n# AFTER (safe)\nallowed_modules = {\u0027datetime\u0027, \u0027time\u0027, \u0027json\u0027, \u0027math\u0027, \u0027re\u0027, \u0027collections\u0027, \u0027itertools\u0027, \u0027pathlib\u0027}\n\n# If os.path is needed, handle it explicitly:\nif isinstance(node, ast.ImportFrom):\n if node.module == \u0027os.path\u0027:\n pass # allow \u0027from os.path import join, exists\u0027 only\n elif node.module and node.module.split(\u0027.\u0027)[0] not in allowed_modules:\n return {\"ok\": False, \"error\": f\"Import of \u0027{node.module}\u0027 not allowed.\"}\n\nAdditionally, 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).",
"id": "GHSA-26mh-57q7-jfvr",
"modified": "2026-10-29T16:34:26Z",
"published": "2026-10-29T16:34:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-26mh-57q7-jfvr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-61438"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62248"
},
{
"type": "PACKAGE",
"url": "https://github.com/MervinPraison/PraisonAI"
},
{
"type": "WEB",
"url": "https://www.vulncheck.com/advisories/praisonai-before-remote-code-execution-via-broken-ast-sandbox"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "PraisonAI vulnerable to Remote Code Execution via Broken AST Sandbox"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.