Summary
Dulwich's filter_branch.py CommitFilter._apply_index_filter() is vulnerable to symlink directory traversal. When processing commit history, materialized tree entries (including symlinks) persist in the working directory between commits, allowing a symlink from an ancestor commit to redirect file writes from a descendant commit to arbitrary filesystem locations.
Root Cause
_apply_index_filter() at dulwich/filter_branch.py:212 calls build_index_from_tree(".", tmp_index_path, ...) which materializes all tree entries to the current working directory. The finally block (line 229-230) only cleans up the temporary index file (os.unlink(tmp_index_path)) — NOT the filesystem files written to CWD. When process_commit() processes parents recursively first (line 260), files materialized from ancestor commits persist and affect processing of descendant commits.
On dulwich 1.2.7, build_file_from_blob() has no symlink protection, and validate_path_element only validates name patterns, not filesystem state.
Impact
An attacker can craft a malicious repository where running filter_branch with an index filter writes attacker-controlled content to arbitrary filesystem locations via symlink traversal. This achieves RCE if the write targets .git/hooks/.
Attack Scenario
- Attacker creates a repository where commit history (linearized) has:
- Ancestor commit: tree entry
evil (mode 120000, symlink → /target_dir)
- Descendant commit: tree entry
evil/payload (mode 100644, attacker content)
- Victim clones repository and runs
filter_branch with an index filter
process_commit() processes ancestor first → materializes evil as symlink to /target_dir in CWD
- CWD is NOT cleaned between commits
- Processing descendant:
os.path.exists("./evil") → True (symlink exists). build_file_from_blob(blob, mode, "./evil/payload") → open("./evil/payload", "wb") follows intermediate symlink → writes to /target_dir/payload
Suggested Fix
Clean the CWD between commit iterations in _apply_index_filter(), or verify that no intermediate path components are symlinks before writing files.
Reported by zx (Jace)
References
Summary
Dulwich's
filter_branch.pyCommitFilter._apply_index_filter()is vulnerable to symlink directory traversal. When processing commit history, materialized tree entries (including symlinks) persist in the working directory between commits, allowing a symlink from an ancestor commit to redirect file writes from a descendant commit to arbitrary filesystem locations.Root Cause
_apply_index_filter()atdulwich/filter_branch.py:212callsbuild_index_from_tree(".", tmp_index_path, ...)which materializes all tree entries to the current working directory. Thefinallyblock (line 229-230) only cleans up the temporary index file (os.unlink(tmp_index_path)) — NOT the filesystem files written to CWD. Whenprocess_commit()processes parents recursively first (line 260), files materialized from ancestor commits persist and affect processing of descendant commits.On dulwich 1.2.7,
build_file_from_blob()has no symlink protection, andvalidate_path_elementonly validates name patterns, not filesystem state.Impact
An attacker can craft a malicious repository where running
filter_branchwith an index filter writes attacker-controlled content to arbitrary filesystem locations via symlink traversal. This achieves RCE if the write targets.git/hooks/.Attack Scenario
evil(mode 120000, symlink →/target_dir)evil/payload(mode 100644, attacker content)filter_branchwith an index filterprocess_commit()processes ancestor first → materializesevilas symlink to/target_dirin CWDos.path.exists("./evil")→ True (symlink exists).build_file_from_blob(blob, mode, "./evil/payload")→open("./evil/payload", "wb")follows intermediate symlink → writes to/target_dir/payloadSuggested Fix
Clean the CWD between commit iterations in
_apply_index_filter(), or verify that no intermediate path components are symlinks before writing files.Reported by zx (Jace)
References