# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: Remote Code Execution / OS Command Injection (CWE-78)
- **Flagged Location**: src/praisonai-agents/praisonaiagents/memory/hooks.py:304
- **Vulnerability Description**: Hook commands loaded from workspace hook configuration flow into `subprocess.run()` with `shell=True`, allowing shell metacharacters in the configured command string to be interpreted by the system shell.

## 2. Analysis Logic
(This section is the core of the report — 80% of content. Show your actual investigation process.)

### Step 1: Examining the flagged process-execution sink at src/praisonai-agents/praisonaiagents/memory/hooks.py:279
I read the flagged hook executor with surrounding context. `_execute_script()` copies `hook.command` into `command`, optionally prepends the workspace path, and then passes that string directly to `subprocess.run()` with `shell=True`:

```python
# src/praisonai-agents/praisonaiagents/memory/hooks.py:279-306
279     def _execute_script(
280         self,
281         hook: HookConfig,
282         context: Dict[str, Any]
283     ) -> HookResult:
284         """Execute a script hook."""
...
298             # Resolve command path
299             command = hook.command
300             if not os.path.isabs(command) and not command.startswith("./"):
301                 command = str(self.workspace_path / command)
302             
303             # Execute
304             result = subprocess.run(
305                 command,
306                 shell=True,
```

Analysis: this is a command execution sink. It is not array/list form; the first argument is a string and line 306 explicitly enables shell interpretation. The path handling at lines 299-301 only rewrites relative commands and does not quote, escape, or allowlist the command.

### Step 2: Tracing the command value backward to hook configuration loading
The `HookConfig` dataclass stores a string command, and `_load_config()` reads `.praison/hooks.json` from the workspace, parses JSON, and copies hook values into `HookConfig.command` without validation:

```python
# src/praisonai-agents/praisonaiagents/memory/hooks.py:73-80
73 @dataclass
74 class HookConfig:
75     """Configuration for a single hook."""
76     event: HookEvent
77     command: str
78     timeout: int = 30
79     enabled: bool = True
80     block_on_failure: bool = False
```

```python
# src/praisonai-agents/praisonaiagents/memory/hooks.py:142-177
142     def _load_config(self):
143         """Load hooks configuration from file."""
144         config_path = self.workspace_path / self.CONFIG_FILE.replace("/", os.sep)
...
150             config = json.loads(config_path.read_text(encoding="utf-8"))
...
155             hooks_config = config.get("hooks", {})
156             
157             for event, command in hooks_config.items():
158                 if event not in self._hooks:
159                     self._hooks[event] = []
160                 
161                 if isinstance(command, str):
162                     # Simple command string
163                     self._hooks[event].append(HookConfig(
164                         event=event,
165                         command=command,
166                         timeout=self._global_timeout
167                     ))
168                 elif isinstance(command, dict):
169                     # Detailed config
170                     self._hooks[event].append(HookConfig(
171                         event=event,
172                         command=command.get("command", ""),
173                         timeout=command.get("timeout", self._global_timeout),
174                         enabled=command.get("enabled", True),
175                         block_on_failure=command.get("block_on_failure", False),
176                         pass_input=command.get("pass_input", True)
177                     ))
```

Analysis: `command` originates from a workspace JSON configuration file. Both supported forms copy attacker-controlled JSON string content directly into the field later used by the shell sink. There is no allowlist of command names, no `shlex.quote()`, and no conversion to subprocess argument-list form on this path.

### Step 3: Confirming the hook file and event model are intended runtime configuration
The module declares `.praison/hooks.json` as the runtime hook configuration file and documents command hooks including `pre_run_command` and `post_run_command`:

```python
# src/praisonai-agents/praisonaiagents/memory/hooks.py:14-23
14 Configuration (.praison/hooks.json):
15     {
16         "hooks": {
17             "pre_write_code": "./scripts/lint.sh",
18             "post_write_code": "./scripts/format.sh",
19             "pre_run_command": "./scripts/validate.sh",
20             "post_user_prompt": "./scripts/log.sh"
21         },
22         "timeout": 30,
23         "enabled": true
```

```python
# src/praisonai-agents/praisonaiagents/memory/hooks.py:47-58
47 # Hook event types
48 HookEvent = Literal[
49     "pre_read_code",
50     "post_read_code",
51     "pre_write_code",
52     "post_write_code",
53     "pre_run_command",
54     "post_run_command",
55     "pre_user_prompt",
56     "post_user_prompt",
57     "pre_mcp_tool_use",
58     "post_mcp_tool_use"
```

Analysis: the command source is not a hardcoded internal value. The module is explicitly designed to read command strings from a project/workspace hook configuration file, including command-related hook events.

### Step 4: Tracing execution from public API call to sink
`execute()` lazy-loads the configuration, checks whether hooks are enabled, and for every hook registered for the requested event calls `_execute_script()`:

```python
# src/praisonai-agents/praisonaiagents/memory/hooks.py:222-263
222     def execute(
223         self,
224         event: HookEvent,
225         context: Optional[Dict[str, Any]] = None
226     ) -> HookResult:
...
237         self._ensure_loaded()
238         
239         if not self._enabled:
240             return HookResult(success=True)
...
256         # Execute script hooks
257         if event in self._hooks:
258             for hook in self._hooks[event]:
259                 if not hook.enabled:
260                     continue
261                 
262                 result = self._execute_script(hook, context)
263                 
```

`_ensure_loaded()` directly invokes `_load_config()` on first use:

```python
# src/praisonai-agents/praisonaiagents/memory/hooks.py:136-140
136     def _ensure_loaded(self):
137         """Lazy load hooks on first access."""
138         if not self._loaded:
139             self._load_config()
140             self._loaded = True
```

Analysis: the data flow is complete: workspace JSON config → `HookConfig.command` → `execute()` → `_execute_script()` → `subprocess.run(command, shell=True)`. This crosses multiple method calls and preserves the command string without sanitization.

### Step 5: Checking public reachability and package context
The hooks manager is exported from the package memory module, and the package configuration includes `praisonaiagents*` packages, so this is application/library code rather than test-only code:

```python
# src/praisonai-agents/praisonaiagents/memory/__init__.py:57-65
57     if name == "HooksManager":
58         from .hooks import HooksManager
59         return HooksManager
60     if name == "HookResult":
61         from .hooks import HookResult
62         return HookResult
63     if name == "create_hooks_manager":
64         from .hooks import create_hooks_manager
65         return create_hooks_manager
```

```python
# src/praisonai-agents/praisonaiagents/memory/__init__.py:87-89
87     "HooksManager",
88     "HookResult",
89     "create_hooks_manager",
```

```toml
# src/praisonai-agents/pyproject.toml:5-7
5 [project]
6 name = "praisonaiagents"
7 version = "0.0.180"
```

```toml
# src/praisonai-agents/pyproject.toml:90-92
90 [tool.setuptools.packages.find]
91 where = ["."]
92 include = ["praisonaiagents*"]
```

Analysis: the vulnerable file is packaged source code and is exported as a public manager, not a test fixture or generated/demo-only file.

### Step 6: Checking documented caller pattern
The repository documentation shows users importing `HooksManager`, configuring `.praison/hooks.json`, and executing hooks through `hooks.execute()`:

```markdown
# README.md:317-330
317 ### 9. Hooks (like Windsurf Cascade Hooks)
318 
319 Configure in `.praison/hooks.json`:
320 
321 ```python
322 from praisonaiagents.memory import HooksManager
323 
324 hooks = HooksManager()
325 
326 # Register Python hooks
327 hooks.register("pre_write_code", lambda ctx: print(f"Writing {ctx['file']}"))
328 
329 # Execute hooks
330 result = hooks.execute("pre_write_code", {"file": "main.py"})
```

Analysis: this confirms the intended entry point is public runtime use of `HooksManager.execute()`. When a configured script hook exists for the selected event, the method chain in Steps 2-4 executes the configured command through the shell.

### Step 7: Checking whether safer subprocess patterns exist on this path
A separate shell tool in the same package demonstrates a safer pattern: it splits a command and invokes `subprocess.Popen(..., shell=False)`. The hook executor does not use this safer helper or equivalent logic.

```python
# src/praisonai-agents/praisonaiagents/tools/shell_tools.py:58-80
58         try:
59             # Always split command for safety (no shell execution)
60             # Use shlex.split with appropriate posix flag
61             if platform.system() == 'Windows':
62                 # Use shlex with posix=False for Windows to handle quotes properly
63                 command = shlex.split(command, posix=False)
64             else:
65                 command = shlex.split(command)
...
74             process = subprocess.Popen(
75                 command,
...
79                 shell=False,  # Always use shell=False for security
80                 env=process_env,
```

Analysis: the project already has a no-shell execution pattern, but `hooks.py` bypasses it and uses `subprocess.run(command, shell=True)` at lines 304-306. That leaves shell metacharacters in hook commands active.

### Step 8: Checking local file-write capability relevant to hook configuration tampering
The package also exposes a file-writing tool that can create parent directories and write arbitrary supplied content to a normalized path, guarded by approval:

```python
# src/praisonai-agents/praisonaiagents/tools/file_tools.py:72-93
72     @staticmethod
73     @require_approval(risk_level="high")
74     def write_file(filepath: str, content: str, encoding: str = 'utf-8') -> bool:
...
86         try:
87             # Validate path to prevent traversal attacks
88             safe_path = FileTools._validate_path(filepath)
89             # Create directory if it doesn't exist
90             os.makedirs(os.path.dirname(safe_path), exist_ok=True)
91             with open(safe_path, 'w', encoding=encoding) as f:
92                 f.write(content)
93             return True
```

Analysis: this supports the configuration-tampering part of the threat model when an agent/tool run is allowed to write project files. The root command-injection issue remains in the hook execution path: the configured string is later interpreted by a shell.

### Analysis Process
- Q1: Does user-controllable input reach a command/code execution sink? → YES. `.praison/hooks.json` is read from the workspace at `hooks.py:144-150`, hook command strings are stored at `hooks.py:161-166` and `hooks.py:168-177`, and `execute()` passes them to `_execute_script()` at `hooks.py:257-262`.
- Q2: Is it code evaluation (`eval`/`exec`) with user data? → NO. The sink is process execution via `subprocess.run()` at `hooks.py:304-306`, not Python `eval()`/`exec()`.
- Q3: Is the command executed in array form without shell? → NO. The code passes the string variable `command` at `hooks.py:304-305` and sets `shell=True` at `hooks.py:306`.
- Q4: Is the user input effectively sanitized? → NO. The only transformation is relative path prefixing at `hooks.py:299-301`; the config value is copied into `HookConfig.command` at `hooks.py:163-166` / `hooks.py:170-172` and then reaches the sink without allowlisting, escaping, or argument-list conversion.
- Q5: Is the code in test/demo/dead/generated context? → NO. `HooksManager` is exported by `praisonaiagents.memory` at `memory/__init__.py:57-65` and included in `__all__` at `memory/__init__.py:87-89`; package discovery includes `praisonaiagents*` at `pyproject.toml:90-92`; README runtime usage calls `hooks.execute()` at `README.md:321-330`.
- → Reached leaf: TRUE_POSITIVE

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- Workspace hook configuration is parsed and command strings are copied directly into `HookConfig.command` in `src/praisonai-agents/praisonaiagents/memory/hooks.py:150-166` and `src/praisonai-agents/praisonaiagents/memory/hooks.py:168-177`.
- `execute()` invokes `_execute_script()` for configured script hooks in `src/praisonai-agents/praisonaiagents/memory/hooks.py:256-262`.
- `_execute_script()` executes the command string with `subprocess.run(command, shell=True)` in `src/praisonai-agents/praisonaiagents/memory/hooks.py:299-306`.

## 4. Remediation Suggestions
- Replace shell-string execution with no-shell argument-list execution, e.g. parse a configured command into an executable plus arguments and call `subprocess.run([...], shell=False)`.
- Prefer an explicit hook schema such as `{ "executable": "./scripts/lint.sh", "args": [] }` and validate executable paths against the workspace or an allowlist.
- If shell execution must remain supported, make it opt-in, document the risk, and quote/validate every user-controlled segment; then re-check that shell metacharacters in hook configuration cannot alter command structure unexpectedly.
