Skip to content

fix: prevent path traversal via .worktreeinclude (CWE-22)#2900

Open
sebastiondev wants to merge 2 commits intoNousResearch:mainfrom
sebastiondev:security/fix-worktreeinclude-path-traversal
Open

fix: prevent path traversal via .worktreeinclude (CWE-22)#2900
sebastiondev wants to merge 2 commits intoNousResearch:mainfrom
sebastiondev:security/fix-worktreeinclude-path-traversal

Conversation

@sebastiondev
Copy link
Contributor

@sebastiondev sebastiondev commented Mar 25, 2026

Vulnerability Summary

CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal")

Severity: Low-to-Medium (defense-in-depth hardening)

Data Flow

  1. A user clones a repository containing a malicious .worktreeinclude file (supply-chain attack vector).
  2. The user runs hermes -w (worktree mode) or has worktree: true in config.
  3. _setup_worktree() is called at cli.py:5106 — it reads .worktreeinclude line-by-line.
  4. On main, each entry is used directly in Path(repo_root) / entry and wt_path / entry with zero validation.
  5. An entry like ../../etc/passwd or ../../../.ssh/id_rsa causes shutil.copy2 or os.symlink to operate on paths outside the repo root and worktree.

Exploitability Analysis

The vulnerability is technically real but practically constrained:

  • Both src and dst use the identical entry value, creating a coupling constraint. For src to escape the repo root, entry must contain .. sequences — but the same sequences apply to dst (which starts 2 levels deeper in .worktrees/hermes-XXXX), so the destination also partially escapes.
  • The most realistic attack (e.g., entry ../../../.ssh/id_rsa with repo at /home/user/repos/project) copies the victim's SSH key to an unexpected local filesystem location — but not inside the git worktree, so it won't be committed/pushed. There is no data exfiltration path without additional attacker capabilities.
  • Tracked symlinks result in a self-copy no-op. Gitignored symlinks don't exist after clone.

Preconditions for exploitation:

  1. Victim must clone a malicious repository controlled by the attacker
  2. Victim must run hermes -w (worktree mode) in that repository
  3. The malicious .worktreeinclude must be tracked in the repo
  4. Repo filesystem depth must align with the traversal depth (fragile)

Fix Description

Changed file: cli.py (13 lines added, 3 lines modified)

The fix adds path traversal validation to the .worktreeinclude handler in _setup_worktree():

  1. Resolve both roots up front: repo_root_resolved and wt_path_resolved.
  2. Resolve each entry's src/dst with .resolve() to canonicalize away .. sequences and symlinks.
  3. Validate containment: Check that src stays within repo_root_resolved and dst stays within wt_path_resolved. If either escapes, log a warning and skip the entry.
  4. Symlink target: Use the already-resolved src path for os.symlink() (minor cleanup — no behavioral change for valid entries).

Rationale

  • Follows existing project patterns: The same path traversal guards already exist in tools/skills_hub.py:1058, tools/skills_tool.py:894, tools/skill_manager_tool.py:178, and tools/skills_guard.py:347-351. This fix closes the gap in the .worktreeinclude handler.
  • Minimal and non-breaking: Only 3 files changed. Legitimate .worktreeinclude entries (relative paths within the repo) continue to work identically.
  • Defense-in-depth: Even though exploitation is constrained, validating untrusted input from repo files is a security best practice, especially given the project's own docs warn about running in untrusted repositories.

Diff

 cli.py                                          |  16 +-
 tests/test_worktreeinclude_cwe22_integration.py | 199 ++++++++++++++++++++++++
 tests/test_worktreeinclude_path_traversal.py    | 189 ++++++++++++++++++++++
 3 files changed, 401 insertions(+), 3 deletions(-)

Test Results

Two test suites were added:

1. tests/test_worktreeinclude_path_traversal.py (Unit Tests)

Re-implements the validation logic from cli.py to test edge cases in isolation:

  • Path traversal entries (../../etc/passwd) are skipped
  • Legitimate entries (e.g., .env, config/local.json) are copied normally
  • Comments and blank lines are ignored
  • Symlink-based traversal attempts are blocked
  • Deeply nested traversal (../../../../../../../etc/shadow) is blocked

2. tests/test_worktreeinclude_cwe22_integration.py (Integration Tests)

Calls the real cli._setup_worktree() function against temporary git repos:

  • Traversal entries are rejected — sensitive files outside the repo are never copied into the worktree
  • Legitimate entries are still correctly copied
  • The worktree is created and functional

Disprove Analysis

We attempted to disprove this finding through 8 independent checks:

Check Result
Auth check N/A — local CLI tool, not a network service. Threat model is malicious repo supply-chain attack.
Network check N/A — no server, no CORS, no ALLOWED_HOSTS.
Deployment check No Dockerfile/k8s. CLI installed directly on the user's machine, runs as user. .worktreeinclude processing happens in Python before any container setup.
Caller trace _setup_worktree() called exactly once at cli.py:5106 when worktree mode is enabled. Entries come directly from the cloned repo — attacker-controlled in supply-chain scenario.
Validation check On main: ZERO validation on .worktreeinclude entries. No sanitize, validate, escape, clean, whitelist, or allowlist.
Prior reports No prior issues about .worktreeinclude path traversal. Several generic security feature requests exist but none for this specific vulnerability.
Security policy No SECURITY.md file exists in the repository.
Recent commits No prior security fix for this specific pattern.

Existing Mitigations Found

  1. src/dst coupling — both paths use the same entry, preventing independent control
  2. wt_path depth — worktree is 2 levels deeper, limiting destination escape
  3. Symlink self-copy — for tracked symlinks, shutil.copy2 resolves both to the same target
  4. No automatic exfiltration — files outside the worktree won't be committed/pushed
  5. Opt-in feature — worktree mode requires explicit -w flag
  6. tools/file_operations.py write deny list — protects .ssh/, .aws/ from agent's write tool (but not from shutil.copy2 in cli.py)

Prior Art

  • CWE-22 is well-documented; similar patterns exist in Git submodule handling (CVE-2018-11235, CVE-2022-39253) where malicious repos exploit path traversal during checkout
  • The .worktreeinclude pattern is conceptually similar to .gitmodules exploitation

Verdict

LIKELY_VALID at medium confidence. The CWE-22 code pattern genuinely exists on main. Practical exploitation is severely constrained by src/dst coupling, but the fix provides real defense-in-depth value and follows the project's own established security patterns. The change is minimal, non-breaking, and well-tested.

Entries in .worktreeinclude are now resolved and validated to ensure
both the source path stays within the repo root and the destination
stays within the worktree directory.  Malicious entries containing
"../" components that would resolve outside these boundaries are
logged and skipped.

This prevents a cloned repository from using .worktreeinclude to
read arbitrary files from the host filesystem or create symlinks
to sensitive paths when the --worktree flag is used.

Adds regression tests in test_worktreeinclude_path_traversal.py.
…tree

These tests import the actual _setup_worktree from cli.py (not a
local re-implementation) and verify that path-traversal entries in
.worktreeinclude are rejected while legitimate entries still work.

Test cases:
- Parent directory traversal (../secret.txt)
- Absolute path entries
- Double-dot chain (subdir/../../outside)
- Symlink-based traversal
- Valid file still copied
- Valid directory still symlinked
- Mixed valid + malicious entries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant