Dulwich: Symlink write-through in checkout(paths=[]) via raw os.open bypasses all symlink protections
Description
## 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)**
CVSS v3.1 base metrics
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:HHigh severity
Band computed from the CVSS base score, not the source's own label — so it means the same thing across every feed.
AV
Local
Attack Vector
AC
Low
Attack Complexity
PR
None
Privileges Required
UI
Required
User Interaction
S
Changed
Scope
C
High
Confidentiality
I
High
Integrity
A
High
Availability
Affected
- Vendor
- PyPI
- Product
- dulwich
Versions
- pkg:pypi/dulwich >= 0.24.0, < 1.2.8
Stated as the source expressed them.
References
- advisoryOSV GHSA-8w8g-wq8h-fq33https://osv.dev/vulnerability/GHSA-8w8g-wq8h-fq33
- otherOSV webhttps://github.com/jelmer/dulwich/security/advisories/GHSA-8w8g-wq8h-fq33
- otherOSV webhttps://github.com/jelmer/dulwich/commit/9389fcb5cb9113adfc7f207d8be86a56904db3e8
- vendorOSV packagehttps://github.com/jelmer/dulwich
- otherOSV webhttps://github.com/jelmer/dulwich/releases/tag/dulwich-1.2.8
Related threats
same CWE or vendorCVE-2026-89091 — A flaw was found in ansible-core.
CVE-2026-89091 · 2h ago
CVE-2026-107716 — Banks generates meaningful LLM prompts using a simple template language.
CVE-2026-107716 · 2h ago
CVE-2026-107707 — Intego Antivirus for Windows through 3.0.0.1 contains a link following vulnerability in its optimization module that allows local unprivileged user…
CVE-2026-107707 · 4h ago
CVE-2026-107608 — Improper link resolution before file access in the asset bundling output handling in AWS aws-cdk-lib before 2.267.0 might allow a context-dependent…
CVE-2026-107608 · 4h ago
PraisonAI: Prompt-injection defense blocks only when 3+ detector families fire simultaneously; realistic single-vector injections pass through unblocked
CVE-2026-60086 · 4h ago
PraisonAI: API deploy code generator embeds unescaped YAML fields into Python source
CVE-2026-61433 · 4h ago