Dulwich: Symlink write-through in checkout(paths=[]) via raw os.open bypasses all symlink protections
CVE-2026-114096 · GHSA-8w8g-wq8h-fq33 · published 2 Oct 2026
How serious is it
- Severity: high.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-114096?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
dulwich(PyPI) >= 0.24.0, < 1.2.8
How to check
- Run
pip show dulwichin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
dulwichto 1.2.8 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
Dulwich's `porcelain.checkout(paths=[...])` code path writes files using raw `os.open(file_path, O_WRONLY|O_CREAT|O_TRUNC, mode)` followed by `f.write(obj.data)`. This code path does NOT call `build_file_from_blob()` at all, completely bypassing any symlink protections (including the unreleased d09f8af fix). `os.open` without `O_NOFOLLOW` follows symlinks at both the target file and intermediate directories, allowing arbitrary file writes.
## Root Cause
At `dulwich/porcelain/__init__.py:5661-5675`, the `checkout(paths=[...])` implementation:
```python
file_path = _checked_worktree_path(r, path)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
with os.fdopen(os.open(file_path, flags, mode), "wb") as f:
f.write(obj.data)
```
`_checked_worktree_path()` (line 601-631) only performs name validation — checking that the path doesn't start with `/` or `\\` and that components pass `INVALID_DOTNAMES` checks. It performs zero filesystem symlink detection.
## Impact
An attacker can craft a malicious repository that, when a victim clones it and runs `checkout(paths=[...])`, writes attacker-controlled content (with attacker-controlled permissions) to any filesystem location accessible to the user. Writing to `.git/hooks/post-checkout` achieves RCE on the next git checkout.
## Attack Scenario
1. Attacker creates a repository where HEAD has `trigger` as a symlink (mode 120000, content `../../.git/hooks/post-checkout`), and tag `v1.0` has `trigger` as an executable file (mode 100755, content `#!/bin/sh\nmalicious_payload`)
2. Victim clones the repository — worktree has `trigger` → `../../.git/hooks/post-checkout` (a symlink)
3. Victim runs `porcelain.checkout(repo, target="v1.0", paths=["trigger"])` to restore a specific file from a tag
4. `_checked_worktree_path(r, "trigger")` passes — name validation only, no symlink check
5. `os.open("trigger", O_WRONLY|O_CREAT|O_TRUNC, 0o755)` follows the symlink → opens `.git/hooks/post-checkout` for writing
6. `f.write(obj.data)` writes the malicious payload to the hook
7. Next checkout operation triggers the hook → RCE
## Suggested Fix
Replace the raw `os.open` path with a call to `build_file_from_blob` (once that function is hardened against intermediate symlinks), or add explicit symlink detection: resolve the path with `os.path.realpath()` and verify it stays within the worktree root before opening.
Reported by **zx (Jace)**
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/jelmer/dulwich/security/advisories/GHSA-8w8g-wq8h-fq33
- github.com/jelmer/dulwich/commit/9389fcb5cb9113adfc7f207d8be86a56904db3e8
- github.com/jelmer/dulwich
- github.com/jelmer/dulwich/releases/tag/dulwich-1.2.8
Merged from OSV. Last updated 25 Oct 2026. All vulnerabilities · RSS · JSON