{"id":"CVE-2026-61438","kind":"vulnerability","aliases":["CVE-2026-61438","GHSA-26mh-57q7-jfvr"],"related":[],"title":"PraisonAI vulnerable to Remote Code Execution via Broken AST Sandbox","description":"### 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 '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.\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 — Lines 364–372:\n\npythonallowed_modules = {'os', 'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools'}\nif isinstance(node, ast.Import):\n    for alias in node.names:\n        if alias.name.split('.')[0] not in allowed_modules:\n            return {\"ok\": False, \"error\": f\"Import of '{alias.name}' 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 — not a __import__() call.\n\nProof-of-Concept validation (static — 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 = {'os', 'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools'}\n\nmalicious = \"import os\\nresult = os.system('id')\"\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('.')[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 '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:\n\npython# BEFORE (vulnerable)\nallowed_modules = {'os', 'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools'}\n\n# AFTER (safe)\nallowed_modules = {'datetime', 'time', 'json', 'math', 're', 'collections', 'itertools', 'pathlib'}\n\n# If os.path is needed, handle it explicitly:\nif isinstance(node, ast.ImportFrom):\n    if node.module == 'os.path':\n        pass  # allow 'from os.path import join, exists' only\n    elif node.module and node.module.split('.')[0] not in allowed_modules:\n        return {\"ok\": False, \"error\": f\"Import of '{node.module}' 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).","summary":"PraisonAI's inline Python step in YAML job workflows checks code with an AST allowlist that permits importing the os module, so a script can import os and call os.system. An attacker who supplies or contributes a workflow file can run arbitrary commands with the privileges of the process that runs it.","severity":"high","cvss":7.3,"epss":0.0031,"exploited":false,"exploited_at":null,"products":[{"name":"praisonai","fixed":["4.6.78"],"affected":"< 4.6.78","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["ai-agents","ai-tooling","ci","cwe-184","cwe-693","cwe-78","praisonai","python"],"not_affected_if":["default-config","trusted-workflows-only"],"how_to_check":"- Check the installed version with `pip show praisonai`; versions below `4.6.78` are affected.\n- Search workflow YAML files for `type: job` with `script:` steps containing `import os`.\n- Check whether `PRAISONAI_ALLOW_JOB_WORKFLOWS` is set to `true` in CI or runtime environments.","fix":"- `praisonai` (PyPI): move to `4.6.78` or later.","status":"open","published_at":"2026-07-15T12:18:18+00:00","first_seen_at":"2026-10-29T16:39:03+00:00","triaged_at":"2026-10-29T16:39:42+00:00","updated_at":"2026-10-29T16:56:32+00:00","category":"ai-tooling","confidence":0.85,"triage":{"at":"2026-10-29T16:39:42Z","model":"claude-haiku-5-5","output":{"fix":"- `praisonai` (PyPI): move to `4.6.78` or later.","reach":"unknown","title":"PraisonAI: broken AST sandbox lets YAML workflow scripts run OS commands","impact":"high","summary":"PraisonAI's inline Python step in YAML job workflows checks code with an AST allowlist that permits importing the os module, so a script can import os and call os.system. An attacker who supplies or contributes a workflow file can run arbitrary commands with the privileges of the process that runs it.","audience":"developers","category":"ai-tooling","nicknames":[],"who_cares":["praisonai","python","ci","ai-agents"],"confidence":0.85,"fix_status":"available","needs_auth":"no","worst_case":"arbitrary OS commands run with the calling process privileges","analyst_note":"","how_to_check":"- Check the installed version with `pip show praisonai`; versions below `4.6.78` are affected.\n- Search workflow YAML files for `type: job` with `script:` steps containing `import os`.\n- Check whether `PRAISONAI_ALLOW_JOB_WORKFLOWS` is set to `true` in CI or runtime environments.","plain_action":"","needs_analyst":false,"not_affected_if":["default-config","trusted-workflows-only"],"who_is_affected":"Teams that run PraisonAI workflows with job-type YAML steps enabled, which requires PRAISONAI_ALLOW_JOB_WORKFLOWS=true, especially in CI or where workflow files come from untrusted sources. Installations that never enable job workflows or only run trusted workflows are less exposed.","user_interaction":"yes","needs_special_setup":"yes","severity_if_unknown":"high"},"cost_usd":0.000891,"input_tokens":5912,"output_tokens":599,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-184':31B '-2026':12A,20A '-61438':13A,21A '-693':33B '-78':35B '0':292C,478C '26mh':16A '364':270C '372':271C '57q7':17A 'access':329C 'agent':25B 'ai':24B,27B,252C 'ai-ag':23B 'ai-gener':251C 'ai-tool':26B 'alia':287C,473C 'alias.name':304C,487C 'alias.name.split':291C,477C 'allow':144C,204C,295C,306C,379C,424C,457C,481C,508C,533C,545C,570C 'allowlist':52B,122C,143C,152C 'arbitrari':79B,216C 'ast':9A,51B,105C,118C,151C,170C,378C 'ast.add':407C 'ast.alias':423C 'ast.and':396C 'ast.assign':420C 'ast.attribute':158C,416C 'ast.binop':383C 'ast.boolop':382C 'ast.call':156C,415C 'ast.compare':385C 'ast.constant':387C 'ast.dict':394C 'ast.div':410C 'ast.eq':399C 'ast.expression':381C 'ast.floordiv':412C 'ast.gt':403C 'ast.gte':404C 'ast.ifexp':386C 'ast.import':154C,285C,421C,471C 'ast.importfrom':155C,422C,565C 'ast.in':405C 'ast.keyword':417C 'ast.list':392C 'ast.load':389C 'ast.lt':401C 'ast.lte':402C 'ast.mod':411C 'ast.module':419C 'ast.mult':409C 'ast.name':388C 'ast.not':398C 'ast.noteq':400C 'ast.notin':406C 'ast.or':397C 'ast.parse':441C 'ast.set':395C 'ast.starred':418C 'ast.store':390C 'ast.sub':408C 'ast.subscript':391C 'ast.tuple':393C 'ast.uadd':414C 'ast.unaryop':384C 'ast.usub':413C 'ast.walk':451C 'attack':69B,177C,179C 'b':124C 'block':111C,340C,344C,461C,486C 'broken':8A,139C 'builtin':127C,342C 'bypass':493C,502C 'bytecod':360C 'call':66B,225C,365C,375C 'caus':268C 'check':47B,116C,172C 'ci':29B,200C,236C 'claim':109C 'code':5A,48B,350C 'collect':280C,432C,541C,552C 'command':80B,218C 'common':313C 'compil':361C 'complet':256C 'concept':369C 'configur':260C 'contain':193C 'contribut':73B,182C 'creat':180C 'cve':11A,19A 'cwe':30B,32B,34B 'danger':112C,230C 'datetim':275C,427C,536C,547C 'design':92C 'detail':261C 'download':247C 'e.g':246C 'elif':577C 'els':497C 'environ':237C 'error':176C,300C 'eval':133C 'exec':132C,444C 'execut':6A,95C,174C,208C,219C 'exist':575C 'explicit':146C,561C 'express':98C 'f':301C,460C,484C 'fals':299C,467C,490C 'file':76B,186C 'fix':504C 'follow':163C 'function':108C 'generat':253C 'ghsa':15A 'ghsa-26mh-57q7-jfvr':14A 'handl':559C 'hold':499C 'howev':320C 'id':439C 'impact':266C 'import':55B,63B,131C,142C,194C,302C,334C,345C,354C,364C,435C,485C,523C,573C 'includ':147C,258C 'individu':339C 'inlin':40B,96C 'instruct':257C 'irrelev':347C 'isinst':283C,455C,469C,563C 'itertool':281C,433C,542C,553C 'jfvr':18A 'job':45B,188C,205C 'jobworkflowexecutor._exec_inline_python':90C 'join':574C 'json':277C,429C,538C,549C 'line':269C 'made':376C 'malici':164C,434C,442C 'malicious.yaml':215C 'math':278C,430C,539C,550C 'may':241C 'mean':161C 'mode':443C 'modul':58B,141C,145C,273C,296C,332C,425C,482C,509C,534C,546C 'multi':233C 'multi-ten':232C 'name':464C,524C 'need':558C 'network':374C 'node':119C,159C,284C,380C,449C,456C,458C,463C,470C,564C 'node.module':567C,578C 'node.module.split':580C 'node.names':289C,475C 'none':335C 'nresult':437C 'ok':298C 'omit':130C 'open':135C 'oper':113C,317C 'origin':242C 'os':57B,64B,148C,195C,217C,274C,307C,426C,436C,506C,529C,535C 'os._exit':326C 'os.execvp':323C 'os.getcwd':319C 'os.path':318C,518C,527C,556C,568C,572C 'os.popen':322C 'os.spawn':324C 'os.system':67B,196C,321C,438C 'output':500C 'particular':229C 'pass':168C,446C,466C,489C,496C,569C 'path':316C 'pathlib':554C 'permit':54B,153C 'pipelin':201C 'poc':255C 'praisonai':1A,22A,36B,38B,203C,212C 'print':459C,483C,491C 'privileg':83B,222C 'process':86B,226C,356C 'proof':367C 'proof-of-concept':366C 'provid':125C 'python':37B,41B,97C,359C,530C 'pythonallow':272C 'pythonimport':377C 're':279C,431C,540C,551C 'real':353C 'recommend':503C 'remot':4A,373C 'remov':505C 'replac':511C 'repositori':250C 'reproduc':263C 'restrict':343C 'return':297C 'root':267C 'run':78B,88B,214C 'safe':94C,516C,544C 'sandbox':10A,137C,492C,498C,501C 'scenario':178C 'script':61B,102C,166C,191C 'share':249C 'sourc':245C 'specif':259C,526C 'statement':355C 'static':371C 'step':42B,103C,167C,192C 'submodul':517C 'summari':89C 'suppli':71B 'templat':248C 'tenant':234C 'time':276C,428C,537C,548C 'tool':28B 'tree':440C,452C 'true':207C,447C 'trust':309C 'type':187C,462C 'untrust':244C 'use':104C,314C,351C 'user':198C 'valid':106C,171C,370C,520C 'via':7A,211C 'vulner':2A,532C 'vulnerability._':265C 'whitelist':128C 'without':175C 'workflow':46B,75B,101C,165C,185C,206C,210C,213C,240C,254C 'yaml':44B,100C,184C,239C","nicknames":[],"mitigations":[],"who_affected":"Teams that run PraisonAI workflows with job-type YAML steps enabled, which requires PRAISONAI_ALLOW_JOB_WORKFLOWS=true, especially in CI or where workflow files come from untrusted sources. Installations that never enable job workflows or only run trusted workflows are less exposed.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"first_public_at":"2026-07-15T12:18:18+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-26mh-57q7-jfvr","url":"https://github.com/advisories/GHSA-26mh-57q7-jfvr","severity":"high","published_at":"2026-10-29T16:34:26+00:00","modified_at":"2026-10-29T16:34:28+00:00","first_seen_at":"2026-10-29T16:39:03+00:00","fetched_at":"2026-10-29T16:39:03+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-61438","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61438","severity":null,"published_at":"2026-07-15T12:18:18+00:00","modified_at":"2026-10-24T21:10:00+00:00","first_seen_at":"2026-10-29T16:56:32+00:00","fetched_at":"2026-10-29T16:56:32+00:00","note":null}],"documents":[{"id":17834,"url":"https://api.github.com/repos/MervinPraison/PraisonAI/security-advisories/GHSA-26mh-57q7-jfvr","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":17835,"url":"https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-26mh-57q7-jfvr","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}