Skip to content
Heist/ feed
Back to feed
HighFix available

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

Type: advisorySources: OSV

## 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 `

Products
dulwich
Affected versions
0.24.0, 0.24.1, 0.24.10, 0.24.2, 0.24.3, 0.24.4, 0.24.5, 0.24.6, 0.24.7, 0.24.8, 0.24.9, 0.25.0, 0.25.1, 0.25.2, 1.0.0, 1.1.0, 1.2.0, 1.2.1, 1.2.2, 1.2.3, 1.2.4, 1.2.5, 1.2.6, 1.2.7
Fixed versions
1.2.8
CVEs
CVE-2026-114096
Confirmed fixed CVEs
Not specified
Updated in this feed
31 Oct 2026, 14:09 UTC · Revision 1

Source evidence

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

core-osv · Original source

Published 2 Oct 2026, 18:53 UTC

## 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 `

osv:GHSA-8w8g-wq8h-fq33

dulwich

Fixed versions: 1.2.8

Complete source record
{
  "id": "50e9e6613ae2a25b4881b695e2354cf9010b1634b131f424a33ae2efc86a600f",
  "url": "https://osv.dev/vulnerability/GHSA-8w8g-wq8h-fq33",
  "method": "structured",
  "payload": {
    "kind": "advisory",
    "title": "Dulwich: Symlink write-through in checkout(paths=[]) via raw os.open bypasses all symlink protections",
    "summary": "## 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 `",
    "evidence": "osv:GHSA-8w8g-wq8h-fq33",
    "products": [
      {
        "name": {
          "value": "dulwich",
          "evidence": "/affected/0/package/name"
        },
        "prerequisites": [],
        "fixed_versions": [
          {
            "value": "1.2.8",
            "evidence": "/affected/0/ranges"
          }
        ],
        "affected_versions": [
          {
            "value": "0.24.0",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.1",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.10",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.2",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.3",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.4",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.5",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.6",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.7",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.8",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.24.9",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.25.0",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.25.1",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "0.25.2",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.0.0",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.1.0",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.0",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.1",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.2",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.3",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.4",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.5",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.6",
            "evidence": "/affected/0/versions"
          },
          {
            "value": "1.2.7",
            "evidence": "/affected/0/versions"
          }
        ]
      }
    ],
    "severity": "high",
    "structured": {
      "id": "GHSA-8w8g-wq8h-fq33",
      "aliases": [
        "CVE-2026-114096",
        "GHSA-8w8g-wq8h-fq33"
      ],
      "metrics": [
        {
          "type": "CVSS_V3",
          "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H"
        }
      ],
      "affected": [
        {
          "ranges": [
            {
              "type": "ECOSYSTEM",
              "events": [
                {
                  "introduced": "0.24.0"
                },
                {
                  "fixed": "1.2.8"
                }
              ]
            }
          ],
          "package": {
            "name": "dulwich",
            "purl": "pkg:pypi/dulwich",
            "ecosystem": "PyPI"
          },
          "versions": [
            "0.24.0",
            "0.24.1",
            "0.24.10",
            "0.24.2",
            "0.24.3",
            "0.24.4",
            "0.24.5",
            "0.24.6",
            "0.24.7",
            "0.24.8",
            "0.24.9",
            "0.25.0",
            "0.25.1",
            "0.25.2",
            "1.0.0",
            "1.1.0",
            "1.2.0",
            "1.2.1",
            "1.2.2",
            "1.2.3",
            "1.2.4",
            "1.2.5",
            "1.2.6",
            "1.2.7"
          ],
          "database_specific": {
            "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-8w8g-wq8h-fq33/GHSA-8w8g-wq8h-fq33.json",
            "last_known_affected_version_range": "<= 1.2.7"
          }
        }
      ],
      "modified": "2026-10-21T10:57:28.880982828Z",
      "provider": "osv",
      "references": [
        "https://github.com/jelmer/dulwich/security/advisories/GHSA-8w8g-wq8h-fq33",
        "https://github.com/jelmer/dulwich/commit/9389fcb5cb9113adfc7f207d8be86a56904db3e8",
        "https://github.com/jelmer/dulwich",
        "https://github.com/jelmer/dulwich/releases/tag/dulwich-1.2.8"
      ],
      "provider_metadata": {
        "cwe_ids": [
          "CWE-59",
          "CWE-61"
        ],
        "severity": "HIGH",
        "github_reviewed": true,
        "nvd_published_at": null,
        "github_reviewed_at": "2026-10-02T18:53:35Z"
      }
    },
    "related_cves": [
      {
        "value": "CVE-2026-114096",
        "evidence": "/aliases"
      }
    ],
    "security_relevant": true,
    "severity_evidence": "/database_specific/severity"
  },
  "source_id": "core-osv",
  "document_id": 392988,
  "published_at": "2026-10-02T18:53:35+00:00"
}