# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: business_logic (CWE-284 / CWE-862 — Improper Access Control / Missing Authorization on shell execution gate)
- **Flagged Location**: `src/praisonai/praisonai/ui/chat.py:159` and `src/praisonai/praisonai/ui/code.py:158`
- **Vulnerability Description**: Both Chainlit UI modules unconditionally hardcode `config.approval_mode = "auto"` immediately after `ToolConfig.from_env()` loads the administrator-configured `PRAISON_APPROVAL_MODE`. This silently overrides any `manual` / `scoped` approval setting, defeats the human-in-the-loop approval gate for `acp_execute_command`, and lets an authenticated user prompt the LLM agent to execute arbitrary single shell commands via `subprocess.run(..., shell=True)` in `action_orchestrator.py`.

## 2. Analysis Logic

### Step 1: Examining the flagged override in `src/praisonai/praisonai/ui/chat.py:151-167`
The lazy loader `_get_interactive_tools()` calls `ToolConfig.from_env()` and then immediately overwrites `approval_mode`:

```python
def _get_interactive_tools():
    """Lazy load interactive tools (ACP, LSP, basic) with trust mode."""
    if 'interactive_tools' not in _cached_modules:
        try:
            _profile_start("load_interactive_tools")
            from praisonai.cli.features.interactive_tools import get_interactive_tools, ToolConfig
            config = ToolConfig.from_env()
            config.workspace = os.getcwd()
            config.approval_mode = "auto"  # Trust mode - auto-approve all tool executions   <- line 159
            tools = get_interactive_tools(config=config)
            ...
```

The override is unconditional — there is no env var or conditional that allows the admin to disable it. Next I need to confirm that `ToolConfig.from_env()` actually reads the admin setting, and that this `config` actually drives the approval decision downstream.

### Step 2: Verifying that `from_env()` honors the admin setting in `src/praisonai/praisonai/cli/features/interactive_tools.py:68-105`
The dataclass default is `"auto"`, but `from_env()` explicitly reads `PRAISON_APPROVAL_MODE`:

```python
@dataclass
class ToolConfig:
    ...
    approval_mode: str = "auto"  # auto, manual, scoped          <- line 75

    @classmethod
    def from_env(cls) -> "ToolConfig":
        """Create config from environment variables."""
        config = cls()
        ...
        # Check approval mode
        approval = os.environ.get("PRAISON_APPROVAL_MODE", "")   <- line 101
        if approval in ("auto", "manual", "scoped"):
            config.approval_mode = approval                       <- line 103
        return config
```

So an administrator who sets `PRAISON_APPROVAL_MODE=manual` does get `config.approval_mode == "manual"` from `from_env()` — and then `chat.py:159` / `code.py:158` clobber it back to `"auto"`. The override is real. Next, trace whether this value reaches the approval gate and the shell sink.

### Step 3: Confirming the same override exists in `src/praisonai/praisonai/ui/code.py:150-166`
```python
def _get_interactive_tools():
    """Lazy load interactive tools (ACP, LSP, basic) with trust mode."""
    if 'interactive_tools' not in _cached_modules:
        try:
            _profile_start("load_interactive_tools")
            from praisonai.cli.features.interactive_tools import get_interactive_tools, ToolConfig
            config = ToolConfig.from_env()
            config.workspace = os.environ.get("PRAISONAI_CODE_REPO_PATH", os.getcwd())
            config.approval_mode = "auto"  # Trust mode - auto-approve all tool executions   <- line 158
            tools = get_interactive_tools(config=config)
```

Identical unconditional override. Both UI entry points are affected.

### Step 4: Tracing config → runtime → approval decision in `src/praisonai/praisonai/cli/features/interactive_tools.py:189-206`
`_load_acp_tools()` propagates `config.approval_mode` into `RuntimeConfig` and `InteractiveRuntime`:

```python
runtime_config = RuntimeConfig(
    workspace=config.workspace,
    lsp_enabled=config.lsp_enabled,
    acp_enabled=config.acp_enabled,
    approval_mode=config.approval_mode,                      <- line 202
)
runtime = InteractiveRuntime(runtime_config)
all_tools = create_agent_centric_tools(runtime)
```

`create_agent_centric_tools` then exposes `acp_execute_command` (among others) as an Agent-callable tool.

### Step 5: Verifying the approval gate uses `runtime.config.approval_mode` in `src/praisonai/praisonai/cli/features/agent_tools.py:225-259`
The shell-execution tool routes through `orchestrator.approve_plan(plan, auto=auto_approve)` where `auto_approve` is derived from the (overridden) config:

```python
def acp_execute_command(command: str, cwd: str = None) -> str:
    ...
    async def _execute():
        prompt = f"run command: {command}"
        result = await orchestrator.create_plan(prompt)
        ...
        # Commands require approval
        auto_approve = runtime.config.approval_mode == "auto"   <- line 249
        approved = await orchestrator.approve_plan(plan, auto=auto_approve)   <- line 250
        if not approved:
            return json.dumps({...})
        result = await orchestrator.apply_plan(plan)
```

Because of the hardcoded override, `runtime.config.approval_mode` is always `"auto"`, so `auto_approve` is always `True`.

### Step 6: Verifying `approve_plan` short-circuits on `"auto"` in `src/praisonai/praisonai/cli/features/action_orchestrator.py:273-315`
```python
async def approve_plan(self, plan: ActionPlan, auto: bool = False) -> bool:
    ...
    approval_mode = self.runtime.config.approval_mode
    if approval_mode == "auto" or auto:                        <- line 290
        plan.status = ActionStatus.APPROVED
        for step in plan.steps:
            step.status = ActionStatus.APPROVED
        return True                                            <- line 294 (auto-approve)
    elif approval_mode == "scoped":
        ...  # FILE_DELETE / SHELL_COMMAND still require manual
    else:  # manual
        return False
```

Under the override, `approval_mode == "auto"` is always true → `approve_plan` returns `True` without any user confirmation, even for `SHELL_COMMAND` steps (which `"scoped"` mode would have gated). `apply_plan` is then called unconditionally.

### Step 7: Verifying the shell-execution sink in `src/praisonai/praisonai/cli/features/action_orchestrator.py:429-444`
```python
elif step.action_type == ActionType.SHELL_COMMAND:
    import subprocess
    result = subprocess.run(
        step.target,
        shell=True,                <- line 433
        capture_output=True,
        text=True,
        cwd=str(workspace),
        timeout=30
    )
    return {
        "command": step.target,
        "stdout": result.stdout,
        "stderr": result.stderr,
        "returncode": result.returncode
    }
```

`shell=True` with `step.target` (derived from the user prompt via `create_plan`). No allowlist is enforced at this layer; the only checks that exist are blocklists on command-chaining operators, which the advisory correctly notes do not catch single-argument destructive commands like `rm -rf /home/user/documents`. The data path from "user chat message" → `acp_execute_command(command)` → `orchestrator.create_plan(f"run command: {command}")` → `ActionType.SHELL_COMMAND` step → `subprocess.run(step.target, shell=True)` is fully connected.

### Step 8: Confirming the tool is exposed to authenticated UI users in `src/praisonai/praisonai/ui/chat.py:414-446`
The agent is built with the interactive tools (including `acp_execute_command`) whenever `tools_enabled` is true (default `true` per `chat.py:453`):

```python
if tools_enabled:
    tools = _get_interactive_tools()
    if os.getenv("TAVILY_API_KEY"):
        tools.append(tavily_web_search)

agent = Agent(
    name="PraisonAI Assistant",
    instructions="""... - Command execution (run shell commands) ...""",
    llm=model_name,
    tools=tools if tools else None,
    ...
)
```

And authentication uses default `admin/admin` credentials (`chat.py:384-395`, `code.py:399-407`):
```python
expected_username = os.getenv("CHAINLIT_USERNAME", "admin")
expected_password = os.getenv("CHAINLIT_PASSWORD", "admin")
```

So any user who can log in (trivially with defaults if unchanged) can prompt the agent to invoke `acp_execute_command`, which is auto-approved because of the hardcoded override, and reaches `subprocess.run(..., shell=True)`.

### Step 9: Confirming this is production UI code, not test/demo
`src/praisonai/praisonai/ui/chat.py` and `src/praisonai/praisonai/ui/code.py` are the Chainlit UI entry points launched by `praisonai ui chat` / `praisonai ui code`. They register `@cl.on_chat_start`, `@cl.password_auth_callback`, etc. — these are runtime handlers, not test fixtures. The override is in the live code path executed for every chat session.

### Analysis Process
- Q1: Is there a business constraint that could be violated? → YES. The approval gate (`PRAISON_APPROVAL_MODE` in `manual`/`scoped`) is a security-critical business invariant intended to require human confirmation before dangerous actions such as shell execution. Evidence: `interactive_tools.py:100-103`, `action_orchestrator.py:296-315`.
- Q2: Is the constraint enforced server-side? → NO — it is implemented (`from_env` reads the env var; `approve_plan` branches on `approval_mode`) but is **silently disabled** by the hardcoded `config.approval_mode = "auto"` at `chat.py:159` and `code.py:158`, which run *after* `from_env()`. Even `scoped` mode (which would gate `SHELL_COMMAND`) is bypassed.
- Q3: Can the user manipulate the relevant parameter? → The user does not even need to manipulate a parameter; the admin's own configuration is overridden. The user only has to send a chat message instructing the agent to run a command. The tool is exposed by default (`chat.py:417-436`, `tools_enabled` defaults to `true`).
- Q4: Is the manipulation filtered out (DTO / allowlist / etc.)? → N/A — no manipulation is needed; the break is in the server's own override. Additionally, the only command sanitization present is a blocklist of chaining operators; there is no allowlist, so single destructive commands pass through to `subprocess.run(..., shell=True)` (`action_orchestrator.py:431-438`).
- Q5: Is the code in test/demo/dead/generated context? → NO. `src/praisonai/praisonai/ui/chat.py` and `src/praisonai/praisonai/ui/code.py` are production Chainlit UI entry points with live `@cl.on_chat_start` / `@cl.password_auth_callback` handlers.
- → Reached leaf: **TRUE_POSITIVE**.

## 3. Verdict

**TRUE_POSITIVE**

**Key Evidence:**
- `src/praisonai/praisonai/ui/chat.py:157-159` and `src/praisonai/praisonai/ui/code.py:156-158` unconditionally overwrite `config.approval_mode = "auto"` immediately after `ToolConfig.from_env()` honors `PRAISON_APPROVAL_MODE`, defeating the admin-configured approval gate.
- The overridden value flows through `interactive_tools.py:202` → `RuntimeConfig.approval_mode` → `agent_tools.py:249` (`auto_approve = runtime.config.approval_mode == "auto"`) → `action_orchestrator.py:290` (`if approval_mode == "auto" or auto: return True`), so `approve_plan` always auto-approves, including `SHELL_COMMAND` steps that `scoped` mode would have gated.
- The auto-approved plan reaches `subprocess.run(step.target, shell=True, ...)` at `action_orchestrator.py:431-438`, and the tool is exposed by default to any authenticated UI user (`chat.py:417-436`, `tools_enabled` defaults to `true`), with default `admin/admin` login credentials at `chat.py:384-385` / `code.py:399-400`.

## 4. Remediation Suggestions
- **Remove the hardcoded override** in both `src/praisonai/praisonai/ui/chat.py:159` and `src/praisonai/praisonai/ui/code.py:158`. Let `ToolConfig.from_env()` be the source of truth, so administrators can enforce `PRAISON_APPROVAL_MODE=manual` or `scoped` for the UI. If a "trust mode" is genuinely desired for some deployments, gate it behind an explicit env var (e.g., `PRAISON_UI_TRUST_MODE=1`) rather than forcing it unconditionally.
- **Default to the safer mode in the UI**. `ToolConfig.approval_mode` currently defaults to `"auto"` (`interactive_tools.py:75`); consider defaulting the UI path to `manual` or `scoped` so that simply launching `praisonai ui chat` without an env var does not silently enable auto-approval of shell commands.
- **Strengthen command sanitization** from a blocklist (chaining operators) to an allowlist of base commands (e.g., `shlex.split` + check `os.path.basename(parts[0])` against an allowlist), since `subprocess.run(..., shell=True)` is used and single-argument destructive commands (`rm -rf`, `chmod 777`, `curl`, `python3 -c`, `dd`) bypass the existing blocklist.
- **Re-check after fix**: with `PRAISON_APPROVAL_MODE=manual` set, confirm that `acp_execute_command` returns `{"requires_approval": true}` and does not call `subprocess.run`; with `scoped`, confirm `SHELL_COMMAND` steps require manual approval while file-read steps proceed.
- **Optionally** replace `subprocess.run(..., shell=True)` with `subprocess.run(shlex.split(command), shell=False, ...)` to remove shell-injection risk from any argument that survives the (now allowlist-based) sanitizer.
