Dulwich: Symlink write-through in checkout(paths=[]) via raw os.open bypasses all symlink protections
## 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 sourcePublished 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"
}