fix: prevent path traversal in .worktreeinclude file processing

Resolve .worktreeinclude entries and validate that both the source path
stays within the repository root and the destination path stays within
the worktree directory before copying files or creating symlinks.

A malicious .worktreeinclude in a cloned repository could previously
reference paths like "../../etc/passwd" to copy or symlink arbitrary
files from outside the repo into the worktree.

CWE-22: Improper Limitation of a Pathname to a Restricted Directory
This commit is contained in:
Sebastion 2026-03-15 01:18:45 +00:00 committed by teknium1
parent f9a61a0d9e
commit 12bc86d9c9
2 changed files with 89 additions and 1 deletions

18
cli.py
View file

@ -571,12 +571,28 @@ def _setup_worktree(repo_root: str = None) -> Optional[Dict[str, str]]:
include_file = Path(repo_root) / ".worktreeinclude"
if include_file.exists():
try:
repo_root_resolved = Path(repo_root).resolve()
wt_path_resolved = wt_path.resolve()
for line in include_file.read_text().splitlines():
entry = line.strip()
if not entry or entry.startswith("#"):
continue
src = Path(repo_root) / entry
dst = wt_path / entry
# Prevent path traversal: ensure src stays within repo_root
# and dst stays within the worktree directory
try:
src_resolved = src.resolve()
dst_resolved = dst.resolve(strict=False)
except (OSError, ValueError):
logger.debug("Skipping invalid .worktreeinclude entry: %s", entry)
continue
if not str(src_resolved).startswith(str(repo_root_resolved) + os.sep) and src_resolved != repo_root_resolved:
logger.warning("Skipping .worktreeinclude entry outside repo root: %s", entry)
continue
if not str(dst_resolved).startswith(str(wt_path_resolved) + os.sep) and dst_resolved != wt_path_resolved:
logger.warning("Skipping .worktreeinclude entry that escapes worktree: %s", entry)
continue
if src.is_file():
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(src), str(dst))
@ -584,7 +600,7 @@ def _setup_worktree(repo_root: str = None) -> Optional[Dict[str, str]]:
# Symlink directories (faster, saves disk)
if not dst.exists():
dst.parent.mkdir(parents=True, exist_ok=True)
os.symlink(str(src.resolve()), str(dst))
os.symlink(str(src_resolved), str(dst))
except Exception as e:
logger.debug("Error copying .worktreeinclude entries: %s", e)