GitHub Security Advisory: GHSA-p3hw-mv63-rf9w
Package: gix-validate (RUST)
Severity: HIGH (CVSS 0.0)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Vulnerable versions: <= 0.10.0
Patched in: 0.11.1

Summary: gix's submodule name validation bypass + trust inheritance flaw enables path traversal and credential disclosure

Details:
### Summary

Submodule name validation bypass plus missing validation in production code paths allows path traversal via crafted `.gitmodules`. Combined with a trust inheritance flaw in `Submodule::open()`, this enables reading arbitrary git repository configs (including credentials) from traversed paths with full trust (CWE-22, CWE-200).

### Details

**Bug 1: Validation bypass in `gix-validate/src/submodule.rs` (lines 27-42)**

The `name()` function uses `name.find(b"..")` which returns only the FIRST occurrence. If the first `..` is embedded in a non-traversal context, the function returns `Ok` without checking subsequent `../` sequences:

```rust
pub fn name(name: &BStr) -> Result<&BStr, name::Error> {
    match name.find(b"..") {
        Some(pos) => {
            let &b = name.get(pos + 2).ok_or(name::Error::ParentComponent)?;
            if b == b'/' || b == b'\\' {
                Err(name::Error::ParentComponent)
            } else {
                Ok(name)  // Returns Ok without checking rest of string
            }
        }
        None => Ok(name),
    }
}
```

Bypass: `a..b/../../../.git/` passes because `find(b"..")` returns position 1 (the `..` in `a..b`), checks `name[3] == b'b'`, and returns Ok. The real `/../../../` is never checked.

**Bug 2: Validation never called in production**

`gix_validate::submodule::name()` has zero production callers (only test code). The `names()` iterator in `gix-submodule/src/access.rs:29` explicitly documents it returns "unvalidated names."

`git_dir()` at `gix/src/submodule/mod.rs:198-204` constructs filesystem paths from raw names:

```rust
pub fn git_dir(&self) -> PathBuf {
    self.state.repo.common_dir().join("modules").join(gix_path::from_bstr(self.name()))
}
```

**Bug 3: Trust inheritance bypass in `Submodule::open()`**

At `gix/src/submodule/mod.rs:270`, `open()` clones the parent repository's options:

```rust
match crate::open_opts(self.git_dir_try_old_form()?, self.state.repo.options.clone()) {
```

The parent's `options.git_dir_trust` is `Some(Trust::Full)`. At `gix/src/open/repository.rs:103-104`:

```rust
if options.git_dir_trust.is_none() {
    options.git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir)?.into();
}
```

Since trust is already `Some(Full)`, the ownership check is **skipped entirely**. The traversed path is opened with `Trust::Full` regardless of ownership, bypassing gitoxide's safe-directory protections.

### PoC

Compiled and executed in Rust 1.94.1 `--release` mode. All bypass cases confirmed:

```
BYPASS a..b/../../../.git/           -> PASSED validation
       git_dir = .git/modules/a..b/../../../.git/
       normalized = .git/              (parent repo!)

BYPASS x..y/../../../.git/config     -> PASSED validation
       git_dir = .git/modules/x..y/../../../.git/config
       normalized = .git/config
```

### Attack chain

1. Attacker crafts a repository with `.gitmodules`:
   ```ini
   [submodule "x..y/../../.."]
       path = innocent
       url = ht