# Dulwich: Symlink write-through in checkout(paths=[]) via raw os.open bypasses all symlink protections

Dulwich checkout(paths=[...]) writes blobs via raw os.open without O_NOFOLLOW, following a repo-planted symlink to overwrite .git/hooks/post-checkout and gain RCE.

- 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-8w8g-wq8h-fq33
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-8w8g-wq8h-fq33
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:path-traversal vuln:rce

**Target:** porcelain.checkout(paths=[...]) file write path in dulwich/porcelain/__init__.py
**Root cause:** checkout(paths=[...]) writes blobs with raw os.open(O_WRONLY|O_CREAT|O_TRUNC) without O_NOFOLLOW, bypassing build_file_from_blob symlink protections; _checked_worktree_path only validates names, not filesystem symlinks.
**Steps:**
1. Craft repo: HEAD has `trigger` as symlink (mode 120000) with content `../../.git/hooks/post-checkout`; tag `v1.0` has `trigger` as mode 100755 file containing `#!/bin/sh\nmalicious_payload`
2. Victim clones repo so worktree `trigger` is the symlink
3. Victim runs `porcelain.checkout(repo, target="v1.0", paths=["trigger"])`
4. `_checked_worktree_path(r, "trigger")` passes name-only validation
5. `os.open("trigger", O_WRONLY|O_CREAT|O_TRUNC, 0o755)` follows symlink and opens `.git/hooks/post-checkout`
6. `f.write(obj.data)` writes payload into the hook
7. Next checkout triggers post-checkout hook → RCE
**How found:** Code review of the checkout(paths=[...]) path revealed it uses raw os.open instead of build_file_from_blob, so the existing symlink hardening (including unreleased d09f8af fix) is never reached; _checked_worktree_path was found to do name validation only.
**Exploitation:** Attacker-controlled repo plants a symlink in the worktree pointing at .git/hooks/post-checkout; a targeted checkout of a tag path follows the link and writes an executable shell payload into the hook, executed on the next checkout.
**Escalation:** Arbitrary file write escalated to code execution by targeting the post-checkout git hook.
**Insight:** When a codebase has a hardened file-write helper, grep for parallel write paths (raw os.open/os.fdopen) that skip it; any os.open without O_NOFOLLOW on a worktree-relative path is a symlink write-through, and .git/hooks/* is the highest-value write target.

