# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: Path Traversal (CWE-22) and Unix Hard Link (CWE-62)
- **Flagged Location**: `src/security/path.rs:137`, `src/tools/filesystem.rs:20-28`
- **Vulnerability Description**: CVE-2026-32232 — Dangling Symlink Component Bypass, TOCTOU Between Validation and Use, and Hardlink Alias Bypass in the path validation logic of ZeptoClaw/PicoClaw filesystem tools.

## 2. Analysis Logic

### Step 1: Examining the path validation sink — `validate_path_in_workspace()`
Read `src/security/path.rs:68-107`. The function resolves the target path, normalizes `..` and `.` components, then canonicalizes via `normalize_path()` and checks `starts_with` against the canonical workspace. If the check passes, it returns a `SafePath`.

```rust
// src/security/path.rs:88-104
let normalized_path = normalize_path(&resolved_path);
let canonical_workspace = workspace_path
    .canonicalize()
    .unwrap_or_else(|_| normalize_path(workspace_path));
if !normalized_path.starts_with(&canonical_workspace) {
    return Err(PicoError::SecurityViolation(...));
}
Ok(SafePath { path: normalized_path })
```

The critical dependency is on `normalize_path()`, which must reliably canonicalize the path to detect all traversal escapes.

### Step 2: Analyzing the `normalize_path()` dangling symlink fallback
Read `src/security/path.rs:117-138`. The function processes `..` and `.` components, then attempts canonicalization:

```rust
// src/security/path.rs:136-137
// Try to canonicalize if the path exists
normalized.canonicalize().unwrap_or(normalized)
```

Line 137 is the core of the Dangling Symlink Component Bypass:

- `std::fs::canonicalize()` resolves all symlinks in the path. If a symlink inside the workspace points to an EXISTING file outside the workspace, canonicalize returns the resolved absolute path, and the `starts_with` check at line 97 correctly rejects it.
- HOWEVER, if a symlink inside the workspace is **dangling** (target does not exist at validation time), `canonicalize()` returns `Err` (I/O error, typically `NotFound`). The `.unwrap_or(normalized)` fallback returns the **non-canonicalized** path — which still contains the unresolved symlink and appears to be within the workspace boundary.
- The `starts_with` check at line 97 passes because the symlink path itself is within the workspace.
- If the symlink target is **later created** (between validation and actual file access), the OS will follow the symlink to the target outside the workspace.

This is the **Dangling Symlink Component Bypass** described in the CVE.

### Step 3: Confirming no hardlink detection exists
Read `src/security/path.rs:144-158` (`contains_traversal_pattern`) and the full `normalize_path()`. Neither function detects hardlinks. `std::fs::canonicalize()` resolves symlinks but does NOT resolve hardlinks (a hardlink IS the file — it shares the same inode but has a different directory entry). If a hardlink to a sensitive file outside the workspace is created inside the workspace, the canonicalization produces the workspace path, and the validation passes. No component-level check detects the hardlink. This is the **Hardlink Alias Bypass** described in the CVE.

### Step 4: Confirming the TOCTOU gap
Read `src/security/path.rs:104-106` and `src/tools/filesystem.rs:20-24`:

```rust
// src/security/path.rs:104-106
Ok(SafePath { path: normalized_path })

// src/tools/filesystem.rs:20-24
fn resolve_path(path: &str, ctx: &ToolContext) -> Result<String> {
    if let Some(ref workspace) = ctx.workspace {
        let safe_path = validate_path_in_workspace(path, workspace)?;
        Ok(safe_path.as_path().to_string_lossy().to_string())
```

The validated path is returned as a plain `String` from `resolve_path()`. File operations use this string later (e.g., `src/tools/filesystem.rs:84` `tokio::fs::read_to_string(&full_path)`). There is no re-validation at the point of actual file access. Between the `validate_path_in_workspace()` call and the `tokio::fs::read_to_string()` call, symlinks can be altered (e.g., a dangling symlink target can be created, or an existing symlink can be replaced). This is the **TOCTOU Between Validation and Use** described in the CVE.

### Step 5: Tracing the user input source
Read `src/tools/filesystem.rs:76-84` (ReadFileTool.execute). The `path` argument is extracted from user-controllable tool call arguments (`args.get("path")`), which originate from the LLM's tool calls at `src/agent/loop.rs:257-268`. The LLM generates these based on user messages from channels (Telegram, Discord, etc.). The path is fully user-influenced.

### Step 6: Checking the bypass when workspace is None
Read `src/tools/filesystem.rs:25-27`:

```rust
} else {
    // No workspace set - allow any path (backwards compatible but less secure)
    Ok(path.to_string())
}
```

When `ctx.workspace` is `None`, ALL path validation is skipped and the raw user-supplied path is passed directly to filesystem operations. While production code at `src/agent/loop.rs:259` always sets the workspace, this code path represents a latent bypass that could be triggered by any consumer of these tools that constructs a `ToolContext` without a workspace.

### Step 7: Verification of code context
The vulnerable code is in production modules (`src/security/path.rs`, `src/tools/filesystem.rs`), not in tests, demos, or examples. The `#[cfg(test)]` blocks are separate from the production logic. The code is actively called from `src/agent/loop.rs:257-259` which constructs the `ToolContext` with the configured workspace.

### Analysis Process
- **Q1: Does user-controllable input influence the file path?** → YES. The `path` argument at `src/tools/filesystem.rs:77-80` comes from LLM tool call arguments, which derive from user messages (step 5).
- **Q2: Is the path canonicalized AND validated against a base directory?** → PARTIALLY. The code at `src/security/path.rs:96-97` performs a `starts_with` check, but the canonicalization at line 137 has a dangling symlink fallback that returns unresolved paths (step 2). Hardlinks are not detected (step 3). A TOCTOU gap exists between validation and use (step 4).
- **Q3: Is basename() / getName() used to strip directory components?** → NO. The code attempts full path canonicalization but has the fallback issue.
- **Q4: Is there an effective allowlist of permitted files?** → NO. There is only a traversal pattern denylist at `src/security/path.rs:144-158`.
- **Q5: Is the code in test/demo/dead/generated context?** → NO. The code is in production modules actively invoked from the agent loop (step 7).
- → **Reached leaf: TRUE_POSITIVE**

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- `src/security/path.rs:137` — `normalized.canonicalize().unwrap_or(normalized)` creates a Dangling Symlink Component Bypass: when a symlink target doesn't exist at validation time, canonicalize fails and falls back to the unresolved symlink path, which passes validation but may later resolve outside the workspace.
- `src/security/path.rs:104-106` combined with `src/tools/filesystem.rs:84` — TOCTOU gap: the validated `SafePath` is returned as a plain string, and file operations use it without re-validation, allowing symlink changes between validation and use.
- `src/security/path.rs:117-138` — No hardlink detection: `canonicalize()` resolves symlinks but not hardlinks, allowing hardlink aliases inside the workspace to provide access to files outside the workspace boundary.

## 4. Remediation Suggestions

1. **Fix the dangling symlink issue in `normalize_path()`**: Instead of `canonicalize().unwrap_or(normalized)`, use `canonicalize()` and **reject** paths that cannot be fully canonicalized, or resolve symlinks component-by-component while verifying each resolved component remains within the workspace boundary.

2. **Add hardlink detection**: After canonicalization, verify that the resolved path's parent directory ancestry does not contain unexpected inode aliases. Alternatively, use `lstat()`-style checks to detect hardlinks by checking the link count on each path component.

3. **Close the TOCTOU window**: Re-validate the path immediately before each file operation (not just once in `resolve_path()`). Consider opening the file handle during validation and passing the handle to the operation, or using `openat2()` with `RESOLVE_NO_SYMLINKS` / `RESOLVE_NO_XDEV` flags (Linux) to prevent symlink and cross-filesystem escapes at the OS level.

4. **Remove or restrict the no-workspace fallback**: The `resolve_path()` at `src/tools/filesystem.rs:25-27` should error or warn when no workspace is configured, rather than silently allowing any path. Consider making `workspace` mandatory or defaulting to a safe temp directory.

5. **Add `O_NOFOLLOW` equivalent**: On Linux, use `O_NOFOLLOW` when opening files to prevent the OS from following symlinks entirely, eliminating the TOCTOU window.
