# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: rce (CWE-78 OS Command Injection)
- **Flagged Location**: `src/praisonai-agents/praisonaiagents/memory/hooks.py:304`
- **Vulnerability Description**: The HooksManager in praisonaiagents reads hook command strings from a workspace `.praison/hooks.json` config file and passes them directly to `subprocess.run(..., shell=True)` without any sanitization, enabling shell metacharacter injection when an attacker can write to (or influence the contents of) the hooks config file.

## 2. Analysis Logic

### Step 1: Examining the flagged sink at hooks.py:304
Read `src/praisonai-agents/praisonaiagents/memory/hooks.py` lines 279-348. The `_execute_script` method builds `command` from `hook.command` and invokes the shell:

```python
command = hook.command
if not os.path.isabs(command) and not command.startswith("./"):
    command = str(self.workspace_path / command)

# Execute
result = subprocess.run(
    command,
    shell=True,
    cwd=str(self.workspace_path),
    env=env,
    capture_output=True,
    text=True,
    timeout=hook.timeout
)
```

- `shell=True` is explicitly set (line 306). The command is a single string, not an argument list.
- Only path normalization (absolute path / `./` prefix) is performed; no shell metacharacter escaping.
- Next: trace where `hook.command` originates.

### Step 2: Tracing `hook.command` to its source in `_load_config`
Read `hooks.py:142-191`. The config is loaded from `self.workspace_path / ".praison/hooks.json"` (CONFIG_FILE at line 106). The command string is taken verbatim from JSON values and stored on `HookConfig.command`:

```python
config_path = self.workspace_path / self.CONFIG_FILE.replace("/", os.sep)
...
config = json.loads(config_path.read_text(encoding="utf-8"))
...
hooks_config = config.get("hooks", {})
for event, command in hooks_config.items():
    if event not in self._hooks:
        self._hooks[event] = []
    if isinstance(command, str):
        # Simple command string
        self._hooks[event].append(HookConfig(
            event=event,
            command=command,
            timeout=self._global_timeout
        ))
    elif isinstance(command, dict):
        self._hooks[event].append(HookConfig(
            event=event,
            command=command.get("command", ""),
            ...
        ))
    elif isinstance(command, list):
        for cmd in command:
            if isinstance(cmd, str):
                self._hooks[event].append(HookConfig(
                    event=event,
                    command=cmd,
                    ...
                ))
```

The JSON `command` value flows unmodified into `HookConfig.command` and then into `subprocess.run(shell=True)`. The file is a workspace resource writable by the agent/user operating in that workspace — exactly the attack surface described in the CVE (file-write via prompt injection or any local write primitive overwrites `.praison/hooks.json`, then the payload executes on the next lifecycle event).

### Step 3: Checking for sanitizers / safe API anywhere in hooks.py
Searched `src/praisonai-agents/praisonaiagents/memory/hooks.py` and the whole `praisonaiagents` package for `shlex`, `allowlist`, `whitelist`, `escapeshell`:

```
Grep shlex|allowlist|whitelist|escapeshell across src/praisonai-agents/praisonaiagents
→ matches only in mcp/mcp.py:274-279 and tools/shell_tools.py:60-65
→ NO matches in memory/hooks.py
```

The `mcp.py` and `shell_tools.py` modules use `shlex.split` for *other* features, but `hooks.py` performs no escaping, no splitting, and no allowlist validation. The command is a raw shell string.

### Step 4: Confirming the public/production status of HooksManager
Read `src/praisonai-agents/praisonaiagents/memory/__init__.py:25-89`. `HooksManager`, `HookResult`, and `create_hooks_manager` are explicitly re-exported and listed in `__all__`:

```python
if name == "HooksManager":
    from .hooks import HooksManager
    return HooksManager
...
__all__ = [
    ...
    "HooksManager",
    "HookResult",
    "create_hooks_manager",
]
```

The README (`src/praisonai-agents/README.md:511-518`) documents `HooksManager` as a user-facing API and instructs users to configure it via `.praison/hooks.json`. This is production framework code, not test/demo/dead code.

### Step 5: Confirming reachability of the sink from the public API
Read `hooks.py:222-277`. The public `execute(event, context)` method iterates registered script hooks and calls `_execute_script` for each (line 262):

```python
# Execute script hooks
if event in self._hooks:
    for hook in self._hooks[event]:
        if not hook.enabled:
            continue
        result = self._execute_script(hook, context)
```

So any caller that constructs `HooksManager(workspace_path=...)` and invokes `execute(...)` reaches the vulnerable sink. The class is designed to fire hooks on agent lifecycle events (`pre_run_command`, `post_run_command`, `pre_write_code`, etc., per `HookEvent` at lines 48-59). The CVE description's two attack surfaces (hook config event types, and automatic firing on lifecycle events) both terminate at this same sink.

### Step 6: Confirming array-form / list execution is NOT used
The `subprocess.run` call passes `command` as a single positional string argument with `shell=True`. There is no `subprocess.run([cmd, *args], shell=False)` list form anywhere in `hooks.py`. Per the decision tree, this is the unsafe shell-string pattern.

### Analysis Process
- Q1: Does user-controllable input reach a command execution sink? → YES. The `command` string sinked at `hooks.py:304-312` is read verbatim from the workspace file `.praison/hooks.json` (`hooks.py:144-186`). Any entity with file-write access to the workspace (including a prompt-injected agent, per the CVE description) controls that string.
- Q2: Is it code evaluation (`eval`/`exec`) with user data? → NO. It is process execution via `subprocess.run`.
- Q3: Is the command executed in array form without shell? → NO. `shell=True` is set explicitly (`hooks.py:306`) and the command is a single string.
- Q4: Is the user input effectively sanitized (allowlist, shlex.quote, escapeshellarg)? → NO. No `shlex`/allowlist/escaping is present in `hooks.py` (Step 3).
- Q5: Is the code in test/demo/dead/generated context? → NO. `HooksManager` is publicly exported in `memory/__init__.py` and documented in the README (Step 4).
- → Reached leaf: TRUE_POSITIVE

## 3. Verdict

**TRUE_POSITIVE**

**Key Evidence:**
- `src/praisonai-agents/praisonaiagents/memory/hooks.py:304-312` invokes `subprocess.run(command, shell=True, ...)` with the raw hook command string.
- `src/praisonai-agents/praisonaiagents/memory/hooks.py:144-186` loads that command string verbatim from the workspace file `.praison/hooks.json` (`CONFIG_FILE` at line 106) with no validation.
- No `shlex.quote`, allowlist, or escaping exists anywhere in `hooks.py`; the only `shlex` users in the package are `mcp/mcp.py` and `tools/shell_tools.py` (unrelated modules).
- `HooksManager` is publicly exported via `praisonaiagents.memory.__init__.py:57-65,87-89` and documented as a user-facing API in `README.md:511-518`, so this is production framework code, not test or dead code.

## 4. Remediation Suggestions
- Stop invoking a shell. Change the executor to use the array form with `shell=False`, e.g. parse the command with `shlex.split(command)` and call `subprocess.run(shlex.split(command), shell=False, cwd=..., env=..., ...)`. This still respects the config-file contract (users author shell-like command lines) while preventing the framework from launching `/bin/sh -c "<user string>"`.
- Alternatively, treat the configured value as a fixed executable path plus args by storing `command` and `args` as separate JSON fields and executing `subprocess.run([command, *args], shell=False)`.
- Add an explicit allowlist of permitted executables (or a hash/signature check) for hook commands, and reject entries containing shell metacharacters (`;`, `|`, `&`, `` ` ``, `$()`, `>`, `<`) when `shell=True` cannot be avoided.
- Restrict `.praison/hooks.json` permissions and document that it must not be writable by untrusted agents; consider signing the file or storing it outside the agent-writable workspace.
- After remediation, re-verify that `_execute_script` no longer passes `shell=True` and that no other call site in the package reintroduces the pattern (the same module's `create_config` helper at `hooks.py:373-400` writes this file and should be updated consistently).
