{"id":"GHSA-cm62-gvxx-vmxx","summary":"Dulwich: Symlink directory traversal in stash pop allows arbitrary file write via intermediate directory symlinks","details":"## Summary\n\nDulwich's `stash.py:pop()` function is vulnerable to symlink directory traversal, allowing an attacker to write arbitrary files outside the repository worktree when a victim pops a stash in a malicious repository.\n\n## Root Cause\n\nThe `pop()` function at `dulwich/stash.py:236` uses `os.path.exists(parent_dir)` to check if a parent directory exists before writing stashed files. `os.path.exists()` follows symlinks, so when an intermediate directory in the path is a symlink pointing outside the worktree (e.g., `link → ../../.git/hooks`), the check passes and subsequent file writes resolve through the symlink.\n\nThe `validate_path()` function (line 228) only validates path component names against `INVALID_DOTNAMES` — it performs zero filesystem symlink detection. On dulwich 1.2.7 (latest release), `build_file_from_blob()` has no symlink protection whatsoever.\n\n## Impact\n\nAn attacker can craft a malicious repository that, when a victim clones it and performs a stash pop operation, writes attacker-controlled content to arbitrary filesystem locations. Writing to `.git/hooks/post-checkout` achieves Remote Code Execution on the victim's machine on the next git checkout operation.\n\n## Attack Scenario\n\n1. Attacker creates a repository with branch `main` containing `link` (symlink → `../../.git/hooks`) and branch `feature` containing `link/post-checkout` (executable payload)\n2. Victim clones the repository (landing on `main` — symlink `link` exists in worktree)\n3. Victim checks out `feature`, makes changes, runs `stash.push()`\n4. Victim checks out `main` (restoring the `link` symlink)\n5. Victim runs `stash.pop(0)` — stash contains `link/post-checkout`\n6. `os.path.exists(\"link\")` returns True (symlink to existing directory), `os.makedirs` skipped\n7. `build_file_from_blob(blob, mode, \"link/post-checkout\")` → `open(\"link/post-checkout\", \"wb\")` follows the intermediate symlink → payload written to `.git/hooks/post-checkout`\n8. Next checkout operation triggers the hook → RCE\n\n## Suggested Fix\n\nBefore writing any file, verify that no component of the target path resolves through a symlink outside the worktree. Use `os.path.realpath(parent_dir)` and confirm it stays within the repository root. Alternatively, use `os.open()` with `O_NOFOLLOW` on each path component.\n\nReported by **zx (Jace)**","modified":"2026-10-02T19:00:06.123601846Z","published":"2026-10-02T18:52:53Z","database_specific":{"cwe_ids":["CWE-22","CWE-59"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-10-02T18:52:53Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/jelmer/dulwich/security/advisories/GHSA-cm62-gvxx-vmxx"},{"type":"WEB","url":"https://github.com/jelmer/dulwich/commit/40a542cb02f9ac39a9eeac1193472903098698f9"},{"type":"PACKAGE","url":"https://github.com/jelmer/dulwich"},{"type":"WEB","url":"https://github.com/jelmer/dulwich/releases/tag/dulwich-1.2.8"}],"affected":[{"package":{"name":"dulwich","ecosystem":"PyPI","purl":"pkg:pypi/dulwich"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.22.5"},{"fixed":"1.2.8"}]}],"versions":["0.22.5","0.22.6","0.22.7","0.22.8","0.23.0","0.23.1","0.23.2","0.24.0","0.24.1","0.24.10","0.24.2","0.24.3","0.24.4","0.24.5","0.24.6","0.24.7","0.24.8","0.24.9","0.25.0","0.25.1","0.25.2","1.0.0","1.1.0","1.2.0","1.2.1","1.2.2","1.2.3","1.2.4","1.2.5","1.2.6","1.2.7"],"database_specific":{"last_known_affected_version_range":"\u003c= 1.2.7","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-cm62-gvxx-vmxx/GHSA-cm62-gvxx-vmxx.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H"}]}