🛡️ CVE-2026-52726 — dulwich
Description
Dulwich's submodule path traversal in porcelain.submodule_update / porcelain.clone(recurse_submodules=True) yields RCE via attacker-dropped .git/hooks payload
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/hooksrather 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 updateCLI /dulwich clone --recurse-submodulesCLI
Vulnerable code
The submodule path from the tree's gitlink entry (and matching .gitmodules) is consumed without validation in [dulwich/porcelain/submodule.py](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fac39e79ca354327897e133/dulwich/porcelain/submodule.py#L154-L234).
The attacker-controlled path enters the loop from iter_cached_submodules ([submodule.py#L154-L168](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fac39e79ca354327897e133/dulwich/porcelain/submodule.py#L154-L168)):
```python
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](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fac39e79ca354327897e133/dulwich/porcelain/submodule.py#L187-L188)):
```python
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](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fac39e79ca354327897e133/dulwich/porcelain/submodule.py#L229-L234)):
```python
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](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fac39e79ca354327897e133/dulwich/config.py#L1637-L1665), 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](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fac39e79ca354327897e133/dulwich/index.py#L2034-L2044) 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](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fac39e79ca354327897e133/dulwich/porcelain/__init__.py#L1548-L1551) calls submodule_update(repo, init=True, recursive=True) once the parent clone completes, reaching the unsanitized loop at [submodule.py#L154-L234](https://github.com/jelmer/dulwich/blob/8efb7d19eac519cd7fa
How this vulnerability can be exploited
This issue can be reached over the network, attack complexity is low, an attacker needs no privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality none, integrity none, availability high.
Weakness class
CVE-2026-52726 is classified as CWE-22: Path Traversal. A file path built from user input is not confined to the intended directory, letting an attacker reach files elsewhere on the filesystem.
Affected software
CVE-2026-52726 is recorded against 2 packages.
- dulwich (from 0.23.2 up to 1.2.5)
- unknown
Timeline and source
Published on 2 July 2026 and last revised on 13 July 2026. No public exploit is currently recorded for this entry. Record sourced from NVD.
References
github.com (Web)
nvd.nist.gov (Advisory)
github.com (Package)
github.com (Web)
CVE-2026-52726 on other distributions
Each distribution ships its own build and its own fixed version. Pick the one you run:
Details
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| dulwich | 0.23.2 | 1.2.5 |
| unknown | — | — |
References
Similar Threats
- Unknown CLSA-2026-1779756522
- Unknown CLSA-2026-1781052734
- Unknown CLSA-2026-1781225617
- Unknown CLSA-2026-1781484847
- Unknown CLSA-2026-1783299701
More CVE 2026 advisories
Browse all of CVE 2026 in the advisory index.
Site Security Check
Is dulwich part of your stack?
CVE-2026-52726 is rated CVSS 8.0 High. BotEraser scans your installation against known CVE records and tells you whether this vulnerability applies to the versions you actually run.
Scan My Site Free →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.