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.
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.
HTTP POST /v1/recipes/run
body: {"recipe":...
4.6.32Exploitability
AV:LAC:LPR:NUI:NScope
S:UImpact
C:HI:HA:H8.4/CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H