# gitoxide gix: untrusted .git/config disables filesystem .git-protection path checks via unfiltered protect_options()

gix's protect_options() reads core.protectNTFS/protectHFS via unfiltered boolean(), so an untrusted repo-local .git/config disables .git path-confusion checks, enabling hook overwrite.

- Payout: Research
- Severity: high
- Program: gitoxide gix
- Found by: manus-use
- Disclosed: 2026-10-23
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-q54r-jf2p-rfr2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-q54r-jf2p-rfr2
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:rust technique:incomplete-fix technique:source-review vuln:access-control vuln:misconfiguration vuln:path-traversal

**Target:** Rust gix crate config cache / path validation
**Root cause:** Cache::protect_options() reads the three path-protection booleans with gix_config::File::boolean(), which delegates to boolean_filter(key, |_| true), skipping the is_trusted section filter applied to every other security-critical read in the same file.
**Steps:**
1. Open a repo owned by another user on a shared filesystem so ownership mismatch maps .git/config sections to Trust::Reduced (gix/src/open/options.rs:215 installs is_trusted filter).
2. Attacker writes `core.protectNTFS = false` and/or `core.protectHFS = false` into the repository-local `.git/config`.
3. Attacker commits tree entries whose names are filesystem-specific encodings of `.git` (e.g. `GIT~1`, trailing-dot/space, ADS forms, HFS+ ignorable code points).
4. Victim runs checkout/index build/merge/tree edit; call sites invoke protect_options() (gix/src/clone/checkout.rs:103, repository/index.rs:206, repository/merge.rs:153/214, object/tree/editor.rs:24).
5. protect_options() reads via self.resolved.boolean(...) at access.rs:306,310,314, ignoring the trust filter; gix_validate::path::component() receives protect_hfs=false/protect_ntfs=false.
6. Crafted entries pass validation; on case-insensitive FS they collapse into real .git, overwriting hooks/config.
**How found:** Code review of gix/src/config/cache/access.rs: noticed every security-critical read uses filter_config_section/boolean_filter except protect_options(), which uses plain boolean(); confirmed boolean() delegates to boolean_filter(key, |_| true) in gix-config comfort.rs:89, and that the path.rs:137 fallback is insufficient.
**Exploitation:** Attacker controls a repo on a shared/network-mounted/CI-reused filesystem, disables NTFS/HFS protection in .git/config, and plants tree entries encoding .git; victim's gix-based tool checks out and writes into .git, overwriting hooks and config, plausibly yielding command execution on next hook-running Git operation.
**Escalation:** Disabling only one protection also defeats the combined-case fallback at gix-validate/src/path.rs:137, which only runs when both are false and only matches exact `.git` case-insensitively, not short-name/ADS/HFS+ encodings.
**Insight:** When auditing trust-boundary code, diff every config read in a security-sensitive module against its siblings: a single accessor swap (boolean vs boolean_filter) silently reopens the boundary. Also check that fallback guards cover the same encodings as the dedicated checks they replace.

## Proof of concept (copied exactly from the original report)

**Rust test showing unfiltered boolean() honors untrusted protectNTFS**

```
// Demonstrates that core.protectNTFS from an untrusted repository-local
// section is honored by the unfiltered read in protect_options().
use std::str::FromStr;

// Trusted (system/global) config: protection on by default.
// Untrusted repository-local config (.git/config) disables NTFS protection.
let repo_local = gix_config::File::from_str("[core]\n    protectNTFS = false").unwrap();

// Reading core.protectNTFS with the trust-aware filter would reject the
// repository-local section; reading it unfiltered (as protect_options does)
// returns Some(false), disabling the .git-confusion check.
let filtered = repo_local.boolean_filter("core.protectNTFS", |_| false); // trusted-only
let unfiltered = repo_local.boolean("core.protectNTFS");                  // what protect_options uses
eprintln!("filtered (trusted only) = {:?}", filtered);   // None  -> safe default kept
eprintln!("unfiltered (vulnerable) = {:?}", unfiltered); // Some(Ok(false)) -> protection disabled
```

**Source of boolean() delegating to boolean_filter with always-true**

```
// gix-config/src/file/access/comfort.rs:89
pub fn boolean(&self, key: impl AsKey) -> Result<Option<bool>> {
    self.boolean_filter(key, |_| true)
}
```

