# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: OS Command Injection (RCE)
- **CWE**: CWE-78
- **Flagged Location**: `src/praisonai-agents/praisonaiagents/memory/hooks.py:304`
- **Vulnerability Description**: The `HooksManager._execute_script()` method passes a command string loaded from a JSON configuration file directly to `subprocess.run(..., shell=True)` without any sanitization, allowing arbitrary shell command execution if an attacker can control the configuration file.

## 2. Analysis Logic

### Step 1: Examining the flagged sink at `src/praisonai-agents/praisonaiagents/memory/hooks.py:304`
```python
# Lines 304-312 in hooks.py:
result = subprocess.run(
    command,
    shell=True,
    cwd=str(self.workspace_path),
    env=env,
    capture_output=True,
    text=True,
    timeout=hook.timeout
)
```
The `subprocess.run()` call uses `shell=True`, which passes the command string to `/bin/sh -c` for shell interpretation. This is a classic command injection sink. The `command` variable is set directly from `hook.command` at line 299 with no sanitization.

### Step 2: Tracing the command origin to the JSON config loader
```python
# Lines 142-177 in hooks.py:
def _load_config(self):
    """Load hooks configuration from file."""
    config_path = self.workspace_path / self.CONFIG_FILE.replace("/", os.sep)
    
    if not config_path.exists():
        return
    
    try:
        config = json.loads(config_path.read_text(encoding="utf-8"))
        
        self._enabled = config.get("enabled", True)
        self._global_timeout = config.get("timeout", 30)
        
        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):
                # Detailed config
                self._hooks[event].append(HookConfig(
                    event=event,
                    command=command.get("command", ""),
                    timeout=command.get("timeout", self._global_timeout),
                    enabled=command.get("enabled", True),
                    block_on_failure=command.get("block_on_failure", False),
                    pass_input=command.get("pass_input", True)
                ))
            elif isinstance(command, list):
                # Multiple commands for same event
                for cmd in command:
                    if isinstance(cmd, str):
                        self._hooks[event].append(HookConfig(
                            event=event,
                            command=cmd,
                            timeout=self._global_timeout
                        ))
```
The `_load_config()` method reads commands from a JSON file at `.praison/hooks.json` (line 106: `CONFIG_FILE = ".praison/hooks.json"`). The `command` values are read directly from the JSON and stored in `HookConfig` objects with no validation, allowlisting, or escaping. Any string in the JSON is accepted as a command.

### Step 3: Tracing the data flow from config to execution
```python
# Lines 279-312 in hooks.py:
def _execute_script(
    self,
    hook: HookConfig,
    context: Dict[str, Any]
) -> HookResult:
    """Execute a script hook."""
    try:
        # Prepare environment
        env = os.environ.copy()
        
        # Pass context as environment variables
        if hook.pass_input:
            for key, value in context.items():
                if isinstance(value, (str, int, float, bool)):
                    env[f"PRAISON_HOOK_{key.upper()}"] = str(value)
            
            # Also pass as JSON
            env["PRAISON_HOOK_CONTEXT"] = json.dumps(context)
        
        # Resolve command path
        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,
            ...
        )
```
The data flow is: `.praison/hooks.json` → `json.loads()` → `HookConfig(command=...)` → `hook.command` → `command` variable → `subprocess.run(command, shell=True, ...)`. At no point is the command sanitized or validated.

### Step 4: Checking for sanitization or validation
I searched the entire `hooks.py` file and found no sanitization, allowlisting, or escaping of the command string before execution. There is no `shlex.quote()`, no allowlist of permitted commands, no path validation beyond prepending the workspace path for relative commands, and no filtering of shell metacharacters.

### Step 5: Checking if the code is in test/demo/dead context
The `HooksManager` class is exported as a public API from `praisonaiagents.memory` (see `memory/__init__.py`). While no production code in this specific repo currently instantiates `HooksManager`, it is a documented, exported library component designed for external consumption. It is not test code, demo code, or dead code in the traditional sense — it is a public API.

### Analysis Process
- Q1: Does user-controllable input reach a command/code execution sink? → **YES**. Commands from `.praison/hooks.json` flow directly to `subprocess.run(..., shell=True)` at `hooks.py:304`.
- Q2: Is it code evaluation (eval/exec) with user data? → **NO**. It is `subprocess.run()` process execution.
- Q3: Is the command executed in array form without shell? → **NO**. `shell=True` is explicitly used at `hooks.py:306`.
- Q4: Is the user input effectively sanitized (allowlist, shlex.quote, escapeshellarg)? → **NO**. No sanitization is performed anywhere in the file.
- Q5: Is the code in test/demo/dead/generated context? → **NO**. It is a public library API exported from `praisonaiagents.memory`.
- → Reached leaf: **TRUE_POSITIVE**

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- `subprocess.run(command, shell=True, ...)` at `src/praisonai-agents/praisonaiagents/memory/hooks.py:304` executes commands from unsanitized JSON config.
- Commands are loaded from `.praison/hooks.json` at `hooks.py:142-177` with no validation or allowlisting.
- `shell=True` at `hooks.py:306` means shell metacharacters are interpreted by `/bin/sh`.
- No sanitization (e.g., `shlex.quote()`, allowlists) is applied to the command string anywhere in the data flow.

## 4. Remediation Suggestions
- **Remove `shell=True`**: If possible, use `subprocess.run(command, shell=False)` with a list of arguments. However, since the command is a single string from the config, this may not be straightforward.
- **Parse commands safely**: Use `shlex.split()` to parse the command string into a list, then pass it as a list to `subprocess.run(..., shell=False)`.
- **Validate commands**: Implement an allowlist of permitted commands or patterns. Reject any command containing shell metacharacters (`;`, `|`, `&&`, `$(...)`, etc.).
- **Restrict config file permissions**: Ensure `.praison/hooks.json` is not writable by untrusted processes (e.g., the agent itself).
- **Consider sandboxing**: Run hook commands in a restricted environment (e.g., Docker container, seccomp, or a minimal sandbox) to limit the impact of command injection.
