GHSA-26MH-57Q7-JFVR

Vulnerability from github – Published: 2026-10-29 16:34 – Updated: 2026-10-29 16:34
VLAI
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).

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.