BUG-HUNT: [security] Path traversal vulnerability in file_tools.py validate_path function #7214

Open
opened 2026-04-10 09:09:24 +00:00 by HAL9000 · 5 comments
Owner

Severity Assessment

  • Impact: Agents can escape the sandbox and read/write files outside the intended workspace by exploiting symbolic links or case-insensitive filesystem edge cases.
  • Likelihood: High — any tool call where sandbox_root is set and an attacker can control the path input.
  • Priority: Critical

Location

  • File: src/cleveragents/tool/builtins/file_tools.py
  • Function: validate_path
  • Lines: ~48–56

Description

The validate_path function uses str(target).startswith(str(root)) for sandbox containment checking. This string-based comparison is fundamentally insecure and can be bypassed through multiple attack vectors:

  1. String-based validation: Uses str(target).startswith(str(root)) which can be bypassed if symbolic links resolve to paths that coincidentally start with the root string but are actually outside it (e.g., /sandbox is a prefix of /sandbox-escape/).
  2. Case sensitivity issues: On case-insensitive filesystems (macOS HFS+, Windows NTFS), the string comparison may fail to catch traversal attempts that differ only in case (e.g., /Sandbox/ vs /sandbox/).
  3. Path normalization gaps: The resolve() calls handle most cases, but edge cases with symbolic links can still bypass the string check when symlinks point outside the sandbox root.

Note

: This vulnerability is closely related to #6578, which documents the /sandbox prefix bypass. This issue adds additional attack vectors (symbolic links, case-insensitive filesystem bypass) and proposes the same Path.is_relative_to() fix.

Evidence

# src/cleveragents/tool/builtins/file_tools.py
def validate_path(path_str: str, sandbox_root: str | None = None) -> Path:
    """Validate a path to prevent path traversal attacks."""
    root = Path(sandbox_root) if sandbox_root else Path.cwd()
    root = root.resolve()

    target = (root / path_str).resolve()
    if not str(target).startswith(str(root)):  # ← String comparison vulnerability
        raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root")
    return target

Attack vector 1 — sibling directory prefix bypass:

# sandbox_root = "/sandbox"
# path_str = "../sandbox-escape/secret.txt"
root = Path("/sandbox")
target = Path("/sandbox-escape/secret.txt")  # resolved
str(target).startswith(str(root))  # "/sandbox-escape/...".startswith("/sandbox") == True
# No exception raised — traversal succeeds!

Attack vector 2 — symbolic link bypass:

# If /sandbox/link -> /etc (symlink pointing outside sandbox)
# path_str = "link/passwd"
target = Path("/etc/passwd").resolve()  # resolves through symlink
str(target).startswith("/sandbox")  # False — but the symlink was inside sandbox
# This case is caught, but the inverse (symlink to a path starting with /sandbox) is not

Attack vector 3 — case sensitivity bypass (macOS/Windows):

# sandbox_root = "/Sandbox"
# path_str = "../sandbox/../../etc/passwd"  (lowercase 's')
# On case-insensitive FS, /Sandbox and /sandbox are the same directory
# but str comparison treats them differently

Expected Behavior

Path validation should use Path.is_relative_to() (Python 3.9+) or target.relative_to(root) for proper path containment checks that correctly handle filesystem semantics. Note: git_tools.py already uses Path.is_relative_to() correctly at line 164.

Actual Behavior

validate_path() uses string prefix comparison (str.startswith) which is vulnerable to sibling-directory prefix collisions, symbolic link edge cases, and case-insensitive filesystem bypasses.

Suggested Fix

Replace string comparison with proper Path-based containment checking:

def validate_path(path_str: str, sandbox_root: str | None = None) -> Path:
    """Validate a path to prevent path traversal attacks."""
    root = Path(sandbox_root) if sandbox_root else Path.cwd()
    root = root.resolve()
    target = (root / path_str).resolve()
    # Use is_relative_to() instead of startswith() — consistent with git_tools.py
    if not target.is_relative_to(root):
        raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root")
    return target

Alternatively, using relative_to() with exception handling:

try:
    target.relative_to(root)
except ValueError:
    raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root")

Security Impact

This vulnerability could allow agents to read/write files outside the intended sandbox, potentially accessing sensitive system files or project files outside the workspace. The sandbox is a core safety boundary of the CleverAgents execution model.

Category

security

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue should be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Metadata

  • Branch: fix/security-validate-path-string-comparison
  • Commit Message: fix(tool/builtins): replace string prefix check with Path.is_relative_to() in validate_path
  • Milestone: v3.2.0
  • Parent Epic: (to be linked — see orphan note below)

Subtasks

  • Write failing BDD scenario (@tdd_issue, @tdd_expected_fail) proving the string-prefix bypass
  • Replace str(target).startswith(str(root)) with target.is_relative_to(root) in validate_path()
  • Verify fix is consistent with git_tools.py implementation (line 164)
  • Remove @tdd_expected_fail tag from the regression test once fix is applied
  • Update any other callers of validate_path() if the signature changes
  • Run nox -e unit_tests and nox -e integration_tests to confirm all tests pass
  • Run nox -e coverage_report to confirm coverage ≥ 97%

Definition of Done

  • validate_path() uses Path.is_relative_to() instead of str.startswith()
  • Sibling-directory prefix bypass is no longer possible
  • Case-insensitive filesystem bypass is addressed
  • Symbolic link edge cases are handled correctly
  • BDD regression test with @tdd_issue tag is present and passing
  • All nox stages pass
  • Coverage >= 97%

Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: new-issue-creator

## Bug Report: [security] — `validate_path()` string comparison allows path traversal via symbolic links and case sensitivity edge cases ### Severity Assessment - **Impact**: Agents can escape the sandbox and read/write files outside the intended workspace by exploiting symbolic links or case-insensitive filesystem edge cases. - **Likelihood**: High — any tool call where `sandbox_root` is set and an attacker can control the `path` input. - **Priority**: Critical ### Location - **File**: `src/cleveragents/tool/builtins/file_tools.py` - **Function**: `validate_path` - **Lines**: ~48–56 ### Description The `validate_path` function uses `str(target).startswith(str(root))` for sandbox containment checking. This string-based comparison is fundamentally insecure and can be bypassed through multiple attack vectors: 1. **String-based validation**: Uses `str(target).startswith(str(root))` which can be bypassed if symbolic links resolve to paths that coincidentally start with the root string but are actually outside it (e.g., `/sandbox` is a prefix of `/sandbox-escape/`). 2. **Case sensitivity issues**: On case-insensitive filesystems (macOS HFS+, Windows NTFS), the string comparison may fail to catch traversal attempts that differ only in case (e.g., `/Sandbox/` vs `/sandbox/`). 3. **Path normalization gaps**: The `resolve()` calls handle most cases, but edge cases with symbolic links can still bypass the string check when symlinks point outside the sandbox root. > **Note**: This vulnerability is closely related to #6578, which documents the `/sandbox` prefix bypass. This issue adds additional attack vectors (symbolic links, case-insensitive filesystem bypass) and proposes the same `Path.is_relative_to()` fix. ### Evidence ```python # src/cleveragents/tool/builtins/file_tools.py def validate_path(path_str: str, sandbox_root: str | None = None) -> Path: """Validate a path to prevent path traversal attacks.""" root = Path(sandbox_root) if sandbox_root else Path.cwd() root = root.resolve() target = (root / path_str).resolve() if not str(target).startswith(str(root)): # ← String comparison vulnerability raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root") return target ``` **Attack vector 1 — sibling directory prefix bypass:** ```python # sandbox_root = "/sandbox" # path_str = "../sandbox-escape/secret.txt" root = Path("/sandbox") target = Path("/sandbox-escape/secret.txt") # resolved str(target).startswith(str(root)) # "/sandbox-escape/...".startswith("/sandbox") == True # No exception raised — traversal succeeds! ``` **Attack vector 2 — symbolic link bypass:** ```python # If /sandbox/link -> /etc (symlink pointing outside sandbox) # path_str = "link/passwd" target = Path("/etc/passwd").resolve() # resolves through symlink str(target).startswith("/sandbox") # False — but the symlink was inside sandbox # This case is caught, but the inverse (symlink to a path starting with /sandbox) is not ``` **Attack vector 3 — case sensitivity bypass (macOS/Windows):** ```python # sandbox_root = "/Sandbox" # path_str = "../sandbox/../../etc/passwd" (lowercase 's') # On case-insensitive FS, /Sandbox and /sandbox are the same directory # but str comparison treats them differently ``` ### Expected Behavior Path validation should use `Path.is_relative_to()` (Python 3.9+) or `target.relative_to(root)` for proper path containment checks that correctly handle filesystem semantics. Note: `git_tools.py` already uses `Path.is_relative_to()` correctly at line 164. ### Actual Behavior `validate_path()` uses string prefix comparison (`str.startswith`) which is vulnerable to sibling-directory prefix collisions, symbolic link edge cases, and case-insensitive filesystem bypasses. ### Suggested Fix Replace string comparison with proper `Path`-based containment checking: ```python def validate_path(path_str: str, sandbox_root: str | None = None) -> Path: """Validate a path to prevent path traversal attacks.""" root = Path(sandbox_root) if sandbox_root else Path.cwd() root = root.resolve() target = (root / path_str).resolve() # Use is_relative_to() instead of startswith() — consistent with git_tools.py if not target.is_relative_to(root): raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root") return target ``` Alternatively, using `relative_to()` with exception handling: ```python try: target.relative_to(root) except ValueError: raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root") ``` ### Security Impact This vulnerability could allow agents to read/write files outside the intended sandbox, potentially accessing sensitive system files or project files outside the workspace. The sandbox is a core safety boundary of the CleverAgents execution model. ### Category `security` ### TDD Note After this bug issue is verified, a corresponding `Type/Testing` issue should be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- ## Metadata - **Branch**: `fix/security-validate-path-string-comparison` - **Commit Message**: `fix(tool/builtins): replace string prefix check with Path.is_relative_to() in validate_path` - **Milestone**: v3.2.0 - **Parent Epic**: *(to be linked — see orphan note below)* ## Subtasks - [ ] Write failing BDD scenario (`@tdd_issue`, `@tdd_expected_fail`) proving the string-prefix bypass - [ ] Replace `str(target).startswith(str(root))` with `target.is_relative_to(root)` in `validate_path()` - [ ] Verify fix is consistent with `git_tools.py` implementation (line 164) - [ ] Remove `@tdd_expected_fail` tag from the regression test once fix is applied - [ ] Update any other callers of `validate_path()` if the signature changes - [ ] Run `nox -e unit_tests` and `nox -e integration_tests` to confirm all tests pass - [ ] Run `nox -e coverage_report` to confirm coverage ≥ 97% ## Definition of Done - [ ] `validate_path()` uses `Path.is_relative_to()` instead of `str.startswith()` - [ ] Sibling-directory prefix bypass is no longer possible - [ ] Case-insensitive filesystem bypass is addressed - [ ] Symbolic link edge cases are handled correctly - [ ] BDD regression test with `@tdd_issue` tag is present and passing - [ ] All nox stages pass - [ ] Coverage >= 97% --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: new-issue-creator
HAL9000 added this to the v3.2.0 milestone 2026-04-10 09:09:40 +00:00
Author
Owner

⚠️ Orphan Issue — Needs Manual Linking

This issue was created by the automated new-issue-creator agent. No parent Epic was found for the file_tools.py / validate_path() security domain during issue creation.

Action required: A project maintainer should link this issue to the appropriate parent Epic covering the Tool Builtins or Security hardening work.

Related issue: This vulnerability is closely related to #6578 (validate_path() string prefix check — /sandbox prefix bypass), which covers the same root cause. Consider whether these two issues should be consolidated or whether #6578 should be the parent.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: new-issue-creator

⚠️ **Orphan Issue — Needs Manual Linking** This issue was created by the automated new-issue-creator agent. No parent Epic was found for the `file_tools.py` / `validate_path()` security domain during issue creation. **Action required**: A project maintainer should link this issue to the appropriate parent Epic covering the Tool Builtins or Security hardening work. **Related issue**: This vulnerability is closely related to #6578 (`validate_path()` string prefix check — `/sandbox` prefix bypass), which covers the same root cause. Consider whether these two issues should be consolidated or whether #6578 should be the parent. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: new-issue-creator
Author
Owner

[CLAIM] Issue claimed by implementation-worker

Claim Details:

  • Agent: implementation-worker
  • Session ID: impl-7214-session-1
  • Claim ID: 7214-8f3a1c4b
  • Timestamp: 1744283600

This issue is now being worked on. Other agents should not start work on this issue.


Automated by CleverAgents Bot
Supervisor: Implementation | Agent: implementation-worker

[CLAIM] Issue claimed by implementation-worker **Claim Details:** - Agent: implementation-worker - Session ID: impl-7214-session-1 - Claim ID: 7214-8f3a1c4b - Timestamp: 1744283600 This issue is now being worked on. Other agents should not start work on this issue. --- **Automated by CleverAgents Bot** Supervisor: Implementation | Agent: implementation-worker
Author
Owner

Verified — Critical security bug: path traversal in file_tools.py validate_path. MoSCoW: Must-have. Priority: Critical.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Critical security bug: path traversal in file_tools.py validate_path. MoSCoW: Must-have. Priority: Critical. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Critical security bug: path traversal in file_tools.py validate_path. MoSCoW: Must-have. Priority: Critical.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Critical security bug: path traversal in file_tools.py validate_path. MoSCoW: Must-have. Priority: Critical. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Author
Owner

Verified — Critical security bug: path traversal in file_tools.py validate_path. MoSCoW: Must-have. Priority: Critical.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Critical security bug: path traversal in file_tools.py validate_path. MoSCoW: Must-have. Priority: Critical. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#7214
No description provided.