---
id: GHSA-8w8g-wq8h-fq33
title: >-
  Dulwich: Symlink write-through in checkout(paths=[]) via raw os.open bypasses
  all symlink protections
summary: >-
  Dulwich: Symlink write-through in checkout(paths=[]) via raw os.open bypasses
  all symlink protections
severity: high
cvss: 8.6
cwe:
  - CWE-59
  - CWE-61
vendor: dulwich
product: dulwich
ecosystem: pip
affected:
  - 'dulwich >= 0.24.0, <= 1.2.7'
patched:
  - dulwich 1.2.8
published: '2026-10-02'
updated: '2026-10-02'
sourceUpdated: '2026-10-02T18:53:36Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-8w8g-wq8h-fq33'
references:
  - url: 'https://github.com/jelmer/dulwich/security/advisories/GHSA-8w8g-wq8h-fq33'
  - url: >-
      https://github.com/jelmer/dulwich/commit/9389fcb5cb9113adfc7f207d8be86a56904db3e8
  - url: 'https://github.com/jelmer/dulwich/releases/tag/dulwich-1.2.8'
  - url: 'https://github.com/advisories/GHSA-8w8g-wq8h-fq33'
tags:
  - ghsa
  - pip
ingestedAt: '2026-10-02T22:33:09.856Z'
---

## Overview

## 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)**

## Affected packages

- `dulwich >= 0.24.0, <= 1.2.7`

## Remediation

Upgrade to a patched release:

- `dulwich 1.2.8`
