attachments: pocs.zip
When Repository::submodules() loads submodule metadata, it prefers the worktree .gitmodules file if that path exists. In the current implementation, the path is read with std::fs::read(), which follows symlinks. As a result, a repository can present a symlinked .gitmodules that points outside the repository, and gitoxide will parse the out-of-repository bytes as submodule configuration.
This is a repository-boundary violation. A caller using the high-level submodule API can believe it is reading repository-local submodule metadata, while the bytes are actually coming from an arbitrary file outside the repository tree.
The relevant flow is:
gix/src/repository/location.rs derives the worktree .gitmodules path as workdir/.gitmodules.gix/src/repository/submodule.rs reads that path with std::fs::read(&path) and immediately parses the bytes as a submodule configuration file.Repository::submodules() exposes the parsed entries through the high-level API.The issue is not in the parser. The issue is that the worktree path is treated as an ordinary file without checking whether it is a symlink, and without checking whether the canonicalized target remains inside the repository worktree.
Because std::fs::read() follows symlinks, a malicious repository can cause gitoxide to ingest bytes from an attacker-chosen location outside the repository. The resulting Submodule objects then expose name, path, and url values derived from that external file.
Use the attached PoC zip that contains the pocs/ workspace.
pocs/F001.Run:
cargo run --quiet
Compare the output with pocs/F001/result.txt.
Important outputs include:
gitmodules_symlink=.../victim-repo/.gitmodulessymlink_target=.../outside/modules.confparsed_name=symlinkedparsed_path=deps/symlinkedparsed_url=https://attacker.example/symlinked.gitThese outputs show that gitoxide parsed the submodule configuration from the symlink target outside the repository, not from repository-local bytes.
Confirmed impact:
Repository::submodules();name, path, and url;This report does not claim direct command execution from this code path by itself. The demonstrated impact is metadata injection across the repository boundary.
A safe fix is to stop silently following symlinks for the worktree .gitmodules path in this loading path.
Reasonable options include:
symlink_metadata() / lstatstyle checks and reject symlinked .gitmodules when loading from the worktree;.gitmodules from the index or HEAD tree rather than following the worktree path.At minimum, the worktree path should not silently follow symlinks to arbitrary external files.
{
"cwe_ids": [
"CWE-22"
],
"github_reviewed_at": "2026-05-05T19:26:09Z",
"github_reviewed": true,
"severity": "HIGH",
"nvd_published_at": null
}