Skip to content

fix(tools): normalize .. in resolve_via_ancestors to prevent sandbox bypass#2130

Merged
bug-ops merged 2 commits intomainfrom
fix-2125-resolve-via-ancestors
Mar 22, 2026
Merged

fix(tools): normalize .. in resolve_via_ancestors to prevent sandbox bypass#2130
bug-ops merged 2 commits intomainfrom
fix-2125-resolve-via-ancestors

Conversation

@bug-ops
Copy link
Owner

@bug-ops bug-ops commented Mar 22, 2026

Summary

Closes #2125.

resolve_via_ancestors silently dropped .. path components when building the non-existing suffix: Path::file_name() returns None for ParentDir, so the walk advanced existing past sandbox boundaries without recording the .. in the suffix. The final base.join(suffix) then produced a path that looked inside the sandbox to starts_with, while the OS would resolve it outside.

Attack vector:

allowed_paths = ["/tmp/sandbox"]
path = "/tmp/sandbox/nonexistent_dir/../../etc/passwd"

starts_with("/tmp/sandbox") returned true, OS opened /etc/passwd.

Fix

Add normalize_path() — a purely lexical stack-based ../. collapse with no filesystem calls — and call it in validate_path before resolve_via_ancestors. All 9 file tool handlers are covered via the single validate_path entry point.

Tests added (9)

  • normalize_path_normal_path — no-op on clean path
  • normalize_path_collapses_dot. eliminated
  • normalize_path_collapses_dotdot — multi-component .. collapsed
  • normalize_path_nested_dotdot — multi-level traversal
  • normalize_path_at_sandbox_boundary — exact boundary preserved
  • validate_path_dotdot_bypass_nonexistent_blocked — primary CVE scenario blocked
  • validate_path_dotdot_nested_bypass_blocked — deeper escape blocked
  • validate_path_inside_sandbox_passes — normal access still allowed
  • validate_path_dot_components_inside_sandbox_passes. inside sandbox allowed

Checks

  • cargo +nightly fmt --check — PASS
  • cargo clippy --workspace --features full -- -D warnings — PASS (0 warnings)
  • cargo nextest run --workspace --features full --lib --bins — 6384 passed, 0 failed

Related

…bypass

resolve_via_ancestors silently dropped `..` components when building the
path suffix (file_name() returns None for ParentDir), allowing a crafted
path like /sandbox/nonexistent/../../etc/passwd to pass the starts_with
check while the OS would resolve it outside the sandbox.

Add normalize_path() for purely lexical collapse of .. and . components
before the ancestor walk. The normalization happens in validate_path
before calling resolve_via_ancestors, covering all 9 file tool handlers.

Closes #2125.
@github-actions github-actions bot added documentation Improvements or additions to documentation rust Rust code changes bug Something isn't working size/M Medium PR (51-200 lines) labels Mar 22, 2026
@bug-ops bug-ops enabled auto-merge (squash) March 22, 2026 14:42
@bug-ops bug-ops merged commit 1d2502e into main Mar 22, 2026
25 checks passed
@bug-ops bug-ops deleted the fix-2125-resolve-via-ancestors branch March 22, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation rust Rust code changes size/M Medium PR (51-200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: resolve_via_ancestors does not normalize .. in suffix, enabling sandbox bypass

1 participant