high · 7.3 Patched

PraisonAI vulnerable to Remote Code Execution via Broken AST Sandbox

CVE-2026-61438 · GHSA-26mh-57q7-jfvr · published 29 Oct 2026

How serious is it

  • Severity: high, CVSS score 7.3 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-61438?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • praisonai (PyPI) < 4.6.78

How to check

  1. Run pip show praisonai in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade praisonai to 4.6.78 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

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

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database. Last updated 30 Oct 2026. All vulnerabilities · RSS · JSON