# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: Remote Code Execution / Shell Command Injection (CWE-78)
- **Flagged Location**: `src/security/shell.rs:218-243`
- **Vulnerability Description**: The `ShellSecurityConfig::validate_command()` function can be bypassed in `Strict` mode via command injection (`;`, `$()`), argument injection (`python3 -P -c "..."`), and shell globbing (`cat /etc/pass[w]d`), allowing execution of dangerous commands.

## 2. Analysis Logic

### Step 1: Examining the flagged sink at `src/security/shell.rs:217-242`
The allowlist check extracts only the first whitespace-separated token and checks if it is in the allowlist:

```rust
// Allowlist check (runs after blocklist)
if self.allowlist_mode != ShellAllowlistMode::Off && !self.allowlist.is_empty() {
    let first_token = command
        .split_whitespace()
        .next()
        .unwrap_or("")
        .to_lowercase();
    // Strip path prefix (e.g. /usr/bin/git -> git)
    let executable = first_token.rsplit('/').next().unwrap_or(&first_token);
    if !self.allowlist.iter().any(|a| a == executable) {
        match self.allowlist_mode {
            ShellAllowlistMode::Strict => {
                return Err(ZeptoError::SecurityViolation(format!(
                    "Command '{}' not in allowlist",
                    executable
                )));
            }
            // ...
        }
    }
}
```

This means a command like `git status; rm -rf /` has first token `git`. If `git` is in the allowlist, this check passes. However, the shell will execute both `git status` AND `rm -rf /` because of the `;` separator.

### Step 2: Tracing command execution to confirm `sh -c` is used
I traced the command execution flow. `ShellTool::execute` (`src/tools/shell.rs:151`) calls `validate_command(command)?`, then passes the command to `self.runtime.execute(command, &container_config)`. The `NativeRuntime::execute` implementation (`src/runtime/native.rs:40-41`) does:

```rust
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(command);
```

The `DockerRuntime` (`src/runtime/docker.rs:177-179`), `AppleContainerRuntime` (`src/runtime/apple.rs:185-187`), `BubblewrapRuntime` (`src/runtime/bubblewrap.rs:62-64`), and `FirejailRuntime` (`src/runtime/firejail.rs:55-57`) all similarly wrap commands with `sh -c`. This means **the entire command string is interpreted by a shell**, making metacharacter injection, command chaining, and glob expansion possible.

### Step 3: Verifying the allowlist bypass with a concrete PoC
Consider `config.validate_command("git status; rm -rf /")` where `git` is allowlisted:
- `first_token` = `git` → matches allowlist → `Ok(())`
- When passed to `sh -c`, the shell executes `git status`, then `rm -rf /`.

This is a genuine bypass. The validator checks the first token only, but the shell executes the whole string.

### Step 4: Verifying the regex blocklist bypass via argument injection
`REGEX_BLOCKED_PATTERNS` includes `r"python[23]?\s+-c\s+"` (`src/security/shell.rs:43`). This regex expects `-c` to immediately follow the `python` invocation (after whitespace). A command like `python3 -P -c "print(1)"` inserts the `-P` flag between `python3` and `-c`. The regex does not match, so the blocklist check passes. However, `sh -c` executes `python3 -P -c "print(1)"`, which is arbitrary Python code execution. The same bypass applies to `perl -e`, `ruby -e`, etc., when flags are inserted.

### Step 5: Verifying the literal blocklist bypass via shell globbing
`LITERAL_BLOCKED_PATTERNS` includes `/etc/passwd` (`src/security/shell.rs:57`). The literal check does a simple substring search (`command_lower.contains(literal)`). A command like `cat /etc/pass[w]d` does not contain the literal string `/etc/passwd`. However, when passed to `sh -c`, the shell expands `[w]` to `w`, so the actual file accessed is `/etc/passwd`. The blocklist is bypassed.

### Step 6: Checking for other callers and context
- `src/tools/shell.rs:151` — `ShellTool` uses `validate_command` before passing to `runtime.execute`.
- `src/tools/custom.rs:117` — `CustomTool` uses `validate_command` before building `tokio::process::Command::new("sh").arg("-c").arg(&command)`.
- `src/plugins/loader.rs:365` — `validate_command_safety` exists but only checks for `&&`, `||`, `;`, `` ` ``, and `|`. This is a separate, more naive check in the plugin loader. It does NOT resolve the fundamental issue in `ShellSecurityConfig`.

There is no dead code or test-only context here. Both `ShellTool` and `CustomTool` are core production tools that can be triggered by the LLM agent.

### Analysis Process
- Q1: Does user-controllable input reach a command execution sink? → YES. The `command` argument in `ShellTool::execute` and `CustomTool::execute` comes from JSON arguments and is passed to `sh -c`.
- Q2: Is it code evaluation (eval/exec) with user data? → NO. It is process execution.
- Q3: Is the command executed in array form without shell? → NO. ALL runtimes use `sh -c`.
- Q4: Is the user input effectively sanitized? → NO. `validate_command` only checks the first token for the allowlist, uses brittle regexes for blocklist, and literal substring matching that misses shell globs.
- Q5: Is the code in test/demo/dead/generated context? → NO. It is active production code.
- → Reached leaf: **TRUE_POSITIVE**

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- `src/security/shell.rs:217-224`: The allowlist only checks the first token (`split_whitespace().next()`), ignoring shell metacharacters in the rest of the command.
- `src/runtime/native.rs:40-41` (and all other runtimes): Commands are executed via `sh -c`, which interprets the full string as shell code, enabling `;`, `&&`, `||`, `$()`, and glob bypasses.
- `src/security/shell.rs:43`: The regex `python[23]?\s+-c\s+` is bypassed by inserting flags like `-P` between `python3` and `-c`.
- `src/security/shell.rs:56-58`: The literal substring check for `/etc/passwd` is bypassed by shell glob syntax such as `/etc/pass[w]d`.

## 4. Remediation Suggestions
- **Do not rely on `sh -c` for security-sensitive command validation.** The validator must understand shell semantics. Consider using a proper shell parser (e.g., `shlex`) to tokenize and analyze the entire AST before execution.
- **Allowlist all tokens, not just the first.** When using `Strict` mode, parse the full command and ensure every executable invoked (after pipes, semicolons, command substitutions) is in the allowlist.
- **Fix regex blocklist for argument injection.** Update patterns like `python[23]?\s+-c\s+` to account for flags between the executable and `-c`, e.g., `python[23]?\b.*\s+-c\s+`.
- **Fix literal blocklist for shell globs.** Before doing substring matching, expand or detect shell glob characters (`[`, `]`, `?`, `*`) that can be used to bypass literal path checks.
- **Prefer array-form execution.** Where possible, use `tokio::process::Command` with explicit arguments instead of `sh -c` to avoid shell interpretation entirely. If `sh -c` semantics are required, implement a strict pre-validation layer that parses the shell syntax rather than relying on regex and substring checks.
