BUG-HUNT: [security] file_tools.validate_path() uses string prefix check — symlinks and case-sensitive FS edge cases can bypass sandbox containment #7423

Closed
opened 2026-04-10 19:12:06 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: Security — Path Traversal Defense Incomplete in file_tools.validate_path()

Severity Assessment

  • Impact: An agent could potentially read/write files outside the sandbox root by exploiting symlinks in the sandbox directory itself or by using special path forms. This breaks the file operation sandboxing guarantee.
  • Likelihood: Low–Medium — requires the sandbox directory to contain symlinks pointing outside, or specific OS-level path manipulation
  • Priority: High (security)

Location

  • File: src/cleveragents/tool/builtins/file_tools.py
  • Function: validate_path()
  • Lines: 51–60

Description

The validate_path() function prevents path traversal via .. components:

def validate_path(path_str: str, sandbox_root: str | None = None) -> Path:
    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)):
        raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root")
    return target

However, there are two issues:

  1. Symlink traversal: Path.resolve() follows symlinks. If the sandbox root itself contains a symlink sandbox/link -> /etc/, then a path like link/passwd will resolve to /etc/passwd, which DOES start with the resolved sandbox root, unless the symlink target is outside. BUT if a symlink inside the sandbox points to a directory within the sandbox that then contains another symlink pointing outside — the string prefix check can fail in degenerate cases depending on path canonicalization.

  2. String prefix check edge case: Consider sandbox_root = /app/sandbox and a path that resolves to /app/sandbox2/secret. The check str(target).startswith(str(root)) would PASS for /app/sandbox against /app/sandbox-extra/file only if the prefix match is exact, BUT on some OS-specific path separator edge cases (e.g., if root doesn't end with /), /app/sandbox is a prefix of /app/sandbox2/secret. For example:

    • root = "/app/sandbox" (no trailing slash)
    • target = "/app/sandbox2/evil"
    • str(target).startswith(str(root))True (INCORRECT!)

Evidence

# src/cleveragents/tool/builtins/file_tools.py, lines 51-60
def validate_path(path_str: str, sandbox_root: str | None = None) -> Path:
    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 prefix check, not path prefix
        raise ValueError(...)
    return target

Test case demonstrating the bug:

sandbox_root = "/app/sandbox"  
# root = Path("/app/sandbox")
# target = Path("/app/sandbox2/evil")
# str(target).startswith(str(root))  → True  (BYPASSES CHECK!)

Expected Behavior

The path containment check should correctly reject paths that escape the sandbox even when the sandbox root name is a prefix of another directory's name.

Actual Behavior

The string prefix check allows paths in sibling directories whose names start with the sandbox root directory name (e.g., /app/sandbox allows /app/sandbox2/evil).

Suggested Fix

def validate_path(path_str: str, sandbox_root: str | None = None) -> Path:
    root = Path(sandbox_root) if sandbox_root else Path.cwd()
    root = root.resolve()
    
    target = (root / path_str).resolve()
    
    # Use Path.is_relative_to() (Python 3.9+) or check with path parts
    try:
        target.relative_to(root)  # raises ValueError if not relative
    except ValueError:
        raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root")
    
    return target

Category

security

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD with tags: @tdd_issue, @tdd_issue_, @tdd_expected_fail.


Automated by CleverAgents Bot
Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor

## Bug Report: Security — Path Traversal Defense Incomplete in file_tools.validate_path() ### Severity Assessment - **Impact**: An agent could potentially read/write files outside the sandbox root by exploiting symlinks in the sandbox directory itself or by using special path forms. This breaks the file operation sandboxing guarantee. - **Likelihood**: Low–Medium — requires the sandbox directory to contain symlinks pointing outside, or specific OS-level path manipulation - **Priority**: High (security) ### Location - **File**: `src/cleveragents/tool/builtins/file_tools.py` - **Function**: `validate_path()` - **Lines**: 51–60 ### Description The `validate_path()` function prevents path traversal via `..` components: ```python def validate_path(path_str: str, sandbox_root: str | None = None) -> Path: 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)): raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root") return target ``` However, there are two issues: 1. **Symlink traversal**: `Path.resolve()` follows symlinks. If the sandbox root itself contains a symlink `sandbox/link -> /etc/`, then a path like `link/passwd` will resolve to `/etc/passwd`, which DOES start with the resolved sandbox root, **unless** the symlink target is outside. BUT if a symlink *inside* the sandbox points to a directory *within* the sandbox that then contains another symlink pointing outside — the string prefix check can fail in degenerate cases depending on path canonicalization. 2. **String prefix check edge case**: Consider `sandbox_root = /app/sandbox` and a path that resolves to `/app/sandbox2/secret`. The check `str(target).startswith(str(root))` would PASS for `/app/sandbox` against `/app/sandbox-extra/file` only if the prefix match is exact, BUT on some OS-specific path separator edge cases (e.g., if `root` doesn't end with `/`), `/app/sandbox` is a prefix of `/app/sandbox2/secret`. For example: - `root = "/app/sandbox"` (no trailing slash) - `target = "/app/sandbox2/evil"` - `str(target).startswith(str(root))` → `True` (INCORRECT!) ### Evidence ```python # src/cleveragents/tool/builtins/file_tools.py, lines 51-60 def validate_path(path_str: str, sandbox_root: str | None = None) -> Path: 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 prefix check, not path prefix raise ValueError(...) return target ``` Test case demonstrating the bug: ```python sandbox_root = "/app/sandbox" # root = Path("/app/sandbox") # target = Path("/app/sandbox2/evil") # str(target).startswith(str(root)) → True (BYPASSES CHECK!) ``` ### Expected Behavior The path containment check should correctly reject paths that escape the sandbox even when the sandbox root name is a prefix of another directory's name. ### Actual Behavior The string prefix check allows paths in sibling directories whose names start with the sandbox root directory name (e.g., `/app/sandbox` allows `/app/sandbox2/evil`). ### Suggested Fix ```python def validate_path(path_str: str, sandbox_root: str | None = None) -> Path: root = Path(sandbox_root) if sandbox_root else Path.cwd() root = root.resolve() target = (root / path_str).resolve() # Use Path.is_relative_to() (Python 3.9+) or check with path parts try: target.relative_to(root) # raises ValueError if not relative except ValueError: raise ValueError(f"Path traversal detected: '{path_str}' escapes sandbox root") return target ``` ### Category security ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD with tags: @tdd_issue, @tdd_issue_<this-issue-number>, @tdd_expected_fail. --- **Automated by CleverAgents Bot** Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor
Author
Owner

Closing as duplicate of #7336. Both issues describe the same vulnerability: validate_path() in file_tools.py uses str(target).startswith(str(root)) (string prefix check) instead of target.is_relative_to(root) (proper path containment check). Issue #7336 was triaged first and assigned to v3.2.0.

The fix described in both issues is identical: use Path.is_relative_to() or target.relative_to(root).


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

Closing as duplicate of #7336. Both issues describe the same vulnerability: `validate_path()` in `file_tools.py` uses `str(target).startswith(str(root))` (string prefix check) instead of `target.is_relative_to(root)` (proper path containment check). Issue #7336 was triaged first and assigned to v3.2.0. The fix described in both issues is identical: use `Path.is_relative_to()` or `target.relative_to(root)`. --- **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#7423
No description provided.