# PraisonAI has unsafe tool resolution in `ToolExecutionMixin.execute_tool`: undeclared `__main__` callables execute

PraisonAI's execute_tool falls back to globals() and __main__ for unresolved tool names, letting attacker-influenced names invoke undeclared callables with app privileges.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: shmulc8
- Disclosed: 2026-05-11
- CVE: CVE-2026-44339
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gmjg-hv98-qggq
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gmjg-hv98-qggq
- Tags: area:ai domain:ai impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:llm tech:python technique:source-review vuln:access-control vuln:rce

**Target:** praisonaiagents ToolExecutionMixin.execute_tool tool-name resolution
**Root cause:** After failing to match declared tools and the registry, execute_tool resolves the name against globals() and __main__ and executes any callable found; the permission gate defaults _perm_allow to None (allow all) instead of restricting to declared tools.
**Steps:**
1. Define a callable only in __main__ (e.g. `def sneaky(msg='ok'): return {'ran': msg}`).
2. Instantiate an agent with empty `tools` list and default `_perm_allow = None`.
3. Call `agent.execute_tool('sneaky', {'msg': 'hello'})` with the undeclared name.
4. Observe `{'ran': 'hello'}` returned and approval registry called with `('demo', 'sneaky', {'msg': 'hello'})`.
**How found:** Code review of the tool resolution fallback chain in tool_execution.py, noticing the globals()/__main__ fallback after registry lookup, then confirming the permission gate treats _perm_allow=None as allow-all via the project's own tests and a local PoC.
**Exploitation:** An attacker who can influence tool-call names (e.g. via prompt injection into an LLM agent) supplies the name of an undeclared callable present in module globals or __main__; execute_tool resolves it and runs it with the application's privileges, enabling unauthorized state changes, data exposure, or command execution depending on loaded helpers.
**Escalation:** Reusing privileged helper functions already in process scope to reach data exposure or command execution.
**Insight:** When auditing agent tool dispatchers, check the resolution order after the declared-tool/registry miss: any globals()/__main__/getattr fallback turns the declared tool list into a non-boundary, and a None allowlist usually means allow-all rather than deny-all.

## Proof of concept (copied exactly from the original report)

**PoC script invoking undeclared __main__ callable**

```
python3 - <<'PY'
import sys
from unittest.mock import MagicMock, patch

sys.path.insert(0, '/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents')
from praisonaiagents.agent.tool_execution import ToolExecutionMixin

def sneaky(msg='ok'):
    return {'ran': msg}

class HookRunner:
    def execute_sync(self, *args, **kwargs):
        return []
    def is_blocked(self, results):
        return False

class Dummy(ToolExecutionMixin):
    def __init__(self):
        self.name = 'demo'
        self.tools = []
        self.chat_history = []
        self._hook_runner = HookRunner()
        self.context_manager = None
        self._doom_loop_tracker = None
        self._perm_deny = frozenset()
        self._perm_allow = None
        self._approval_backend = None

mock_registry = MagicMock()
mock_registry.approve_sync.return_value = MagicMock(approved=True, reason='mock', modified_args=None)
mock_registry.mark_approved = MagicMock()

with patch('praisonaiagents.approval.get_approval_registry', return_value=mock_registry):
    agent = Dummy()
    print(agent.execute_tool('sneaky', {'msg': 'hello'}))
    print(mock_registry.approve_sync.call_args)
PY
```

