# Dulwich: Symlink directory traversal in stash pop allows arbitrary file write via intermediate directory symlinks

Dulwich stash pop follows intermediate directory symlinks, letting a malicious repo write attacker content to .git/hooks and achieve RCE on next checkout.

- Payout: Research
- Severity: high
- Program: Dulwich
- Found by: manus-use
- Disclosed: 2026-10-02
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-cm62-gvxx-vmxx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-cm62-gvxx-vmxx
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:chained technique:source-review vuln:path-traversal vuln:rce

**Target:** dulwich stash.py pop() file write path
**Root cause:** pop() uses os.path.exists(parent_dir) which follows symlinks, and validate_path() only checks component names against INVALID_DOTNAMES with no symlink detection; build_file_from_blob() opens the path without O_NOFOLLOW.
**Steps:**
1. Create repo with branch `main` containing symlink `link` → `../../.git/hooks` and branch `feature` containing executable `link/post-checkout`.
2. Victim clones (lands on `main`, symlink present), checks out `feature`, makes changes, runs `stash.push()`.
3. Victim checks out `main` restoring the `link` symlink, then runs `stash.pop(0)` with stash containing `link/post-checkout`.
4. `os.path.exists("link")` returns True so `os.makedirs` is skipped; `build_file_from_blob(blob, mode, "link/post-checkout")` calls `open("link/post-checkout", "wb")` following the symlink.
5. Payload lands in `.git/hooks/post-checkout`; next checkout triggers hook → RCE.
**How found:** Code review of stash.py pop(): noticed os.path.exists follows symlinks and validate_path only checks dotnames, with no symlink resolution in build_file_from_blob.
**Exploitation:** Attacker-controlled repo plants a symlink to .git/hooks; victim's stash pop writes an executable post-checkout hook through it, executing on the next git checkout.
**Escalation:** Arbitrary file write escalated to RCE by targeting .git/hooks/post-checkout.
**Insight:** When auditing archive/stash extraction, check whether parent-directory existence checks use os.path.exists (follows symlinks) rather than realpath containment or O_NOFOLLOW; intermediate symlinks in the worktree are the pivot.

