All vulnerabilities
GHSA-cm62-gvxx-vmxxCVE-2026-114098

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

dulwichpipLibraries & packages
CVSS
8.6High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in1.2.8Fixed versionGitHub
Published2 Oct 2026Updated 21 Oct 2026GitHub

What fixes it

  • 1.2.8dulwich, pipGitHub
  • 1.2.8dulwich, PyPIOSVpip install "dulwich>=1.2.8"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

Dulwich'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.

Root Cause

The 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.

The 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.

Impact

An 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.

Attack Scenario

  1. Attacker creates a repository with branch main containing link (symlink → ../../.git/hooks) and branch feature containing link/post-checkout (executable payload)
  2. Victim clones the repository (landing on main — symlink link exists in worktree)
  3. Victim checks out feature, makes changes, runs stash.push()
  4. Victim checks out main (restoring the link symlink)
  5. Victim runs stash.pop(0) — stash contains link/post-checkout
  6. os.path.exists("link") returns True (symlink to existing directory), os.makedirs skipped
  7. 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
  8. Next checkout operation triggers the hook → RCE

Suggested Fix

Before 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.

Reported by zx (Jace)

Text from GitHub Advisory Database

Affected versions 33

  • dulwich, pip>= 0.22.5, <= 1.2.7GitHub
  • dulwich, PyPIfrom 0.22.5, fixed in 1.2.8OSV
  • dulwich, PyPI0.22.5OSV
  • dulwich, PyPI0.22.6OSV
  • dulwich, PyPI0.22.7OSV
  • dulwich, PyPI0.22.8OSV
  • dulwich, PyPI0.23.0OSV
  • dulwich, PyPI0.23.1OSV
  • dulwich, PyPI0.23.2OSV
  • dulwich, PyPI0.24.0OSV
  • dulwich, PyPI0.24.1OSV
  • dulwich, PyPI0.24.10OSV
  • dulwich, PyPI0.24.2OSV
  • dulwich, PyPI0.24.3OSV
  • dulwich, PyPI0.24.4OSV
  • dulwich, PyPI0.24.5OSV
  • dulwich, PyPI0.24.6OSV
  • dulwich, PyPI0.24.7OSV
  • dulwich, PyPI0.24.8OSV
  • dulwich, PyPI0.24.9OSV
  • dulwich, PyPI0.25.0OSV
  • dulwich, PyPI0.25.1OSV
  • dulwich, PyPI0.25.2OSV
  • dulwich, PyPI1.0.0OSV
  • dulwich, PyPI1.1.0OSV
  • dulwich, PyPI1.2.0OSV
  • dulwich, PyPI1.2.1OSV
  • dulwich, PyPI1.2.2OSV
  • dulwich, PyPI1.2.3OSV
  • dulwich, PyPI1.2.4OSV
  • dulwich, PyPI1.2.5OSV
  • dulwich, PyPI1.2.6OSV
  • dulwich, PyPI1.2.7OSV

References 4