attachments: pocs.zip
Submodule names coming from .gitmodules are exposed as unvalidated names and are later reused to derive the submodule git directory as:
<superproject common_dir>/modules/<submodule name>
Because the submodule name is joined directly as a filesystem path component, a name such as ../../../escaped-target.git escapes .git/modules after normalization. The current implementation then uses that escaped path in both state() and open().
The updated PoC demonstrates the real sink, not just string construction:
state() reports repository_exists=true for the traversed path;open() returns a repository whose normalized common_dir() matches the attacker-chosen repository outside .git/modules.The relevant flow is:
gix-submodule/src/access.rs exposes unvalidated submodule names from configuration.gix/src/submodule/mod.rs derives the git directory by doing common_dir().join("modules").join(name) with no confinement check.gix/src/submodule/mod.rs uses that derived path during state resolution and repository opening.There is no normalization-and-confinement step between “submodule name from configuration” and “filesystem path used for repository existence checks / open.” As a result, traversal segments in the submodule name directly influence which repository path is inspected and opened.
Use the attached PoC zip that contains the pocs/ workspace.
pocs/F002.Run:
cargo run --quiet
Compare the output with pocs/F002/result.txt.
Key outputs are:
submodule_name=../../../escaped-target.gitderived_git_dir_raw=.../.git/modules/../../../escaped-target.gitderived_git_dir_normalized=.../artifacts/escaped-target.gitescaped_target=.../artifacts/escaped-target.gitrepository_exists=truesubmodule_opened=trueopened_common_dir_normalized=.../artifacts/escaped-target.gitnormalized_git_dir_matches_target=trueopened_common_dir_matches_target=truetarget_outside_modules_root=trueThese outputs show that gitoxide is not only constructing a traversable path string. It is actually using the escaped path for repository existence checks and for opening a repository object.
Confirmed impact:
.git/modules/<name> to an attacker-chosen repository path outside .git/modules;Submodule::state() can report repository existence for the wrong repository;Submodule::open() can return a repository object backed by that attacker-chosen path.This is best described as a path-traversal / repository-confusion issue in submodule repository resolution.
This report does not claim command execution from this behavior alone. The demonstrated impact is repository redirection: callers that enumerate, inspect, or operate on submodules can be steered into using the wrong repository.
Two complementary fixes are advisable:
<common_dir>/modules,In short, submodule names may remain opaque configuration identifiers, but they should not be treated as trusted filesystem subpaths.
{
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T19:27:51Z",
"nvd_published_at": null,
"severity": "HIGH",
"cwe_ids": [
"CWE-22"
]
}