VDB
Sign up
HIGH

GHSA-pg4w-g64p-qwhj

gix and gitoxide's symlinked .gitmodules are followed and parsed from outside of the repository

Details

## Summary attachments: [pocs.zip](https://github.com/user-attachments/files/26431422/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.

## Root cause analysis

The relevant flow is:

1. [`gix/src/repository/location.rs`](https://github.com/GitoxideLabs/gitoxide/blob/v0.52.0/gix/src/repository/location.rs) derives the worktree `.gitmodules` path as `workdir/.gitmodules`. 2. [`gix/src/repository/submodule.rs`](https://github.com/GitoxideLabs/gitoxide/blob/v0.52.0/gix/src/repository/submodule.rs) reads that path with `std::fs::read(&path)` and immediately parses the bytes as a submodule configuration file. 3. `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.

## Reproduction steps

Use the attached PoC zip that contains the `pocs/` workspace.

1. Unzip the PoC archive. 2. Enter `pocs/F001`. 3. Run: ```bash cargo run --quiet ``` 4. Compare the output with `pocs/F001/result.txt`.

Important outputs include:

- `gitmodules_symlink=.../victim-repo/.gitmodules` - `symlink_target=.../outside/modules.conf` - `parsed_name=symlinked` - `parsed_path=deps/symlinked` - `parsed_url=https://attacker.example/symlinked.git`

These outputs show that gitoxide parsed the submodule configuration from the symlink target outside the repository, not from repository-local bytes.

## Impact

Confirmed impact:

- out-of-repository bytes can be injected into the result of `Repository::submodules()`; - callers can be misled about submodule metadata such as `name`, `path`, and `url`; - any downstream workflow that uses those values to decide clone, fetch, update, or policy behavior is operating on attacker-controlled data that did not actually originate from the repository tree.

This report does **not** claim direct command execution from this code path by itself. The demonstrated impact is metadata injection across the repository boundary.

## Recommended fix

A safe fix is to stop silently following symlinks for the worktree `.gitmodules` path in this loading path.

Reasonable options include:

1. use `symlink_metadata()` / `lstat`style checks and reject symlinked `.gitmodules` when loading from the worktree; 2. canonicalize the target and verify that it still resides under the repository worktree before reading it; 3. for security-sensitive callers, prefer loading `.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.

Are you affected?

Enter the version of the package you're using.

Affected packages

crates.io/gitoxide
Introduced in: 0Fixed in: 0.52.1

Upgrade gitoxide to 0.52.1 or newer (ecosystem crates.io).

crates.io/gix
Introduced in: 0Fixed in: 0.83.0

Upgrade gix to 0.83.0 or newer (ecosystem crates.io).

References