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


- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Fix available:** Yes
- **Published:** 23 Oct 2026

## What is GHSA-q54r-jf2p-rfr2?

GHSA-q54r-jf2p-rfr2 is a vulnerability in gix, published on 23 Oct 2026. 

### Official description

## Summary

In the `gix` crate, `Cache::protect_options()` in `gix/src/config/cache/access.rs` reads the three filesystem path-protection settings (`core.protectHFS`, `core.protectNTFS`, and `gitoxide.core.protectWindows`) with the unfiltered `gix_config::File::boolean()` accessor instead of the trust-aware `boolean_filter()` accessor used by every other security-critical config read in the same file. When a repository is opened with reduced trust (an ownership mismatch, which maps `.git/config` sections to `Trust::Reduced`), these protection settings should not be honored from the untrusted repository-local config. Because the trust filter is skipped, an attacker who controls a repository's `.git/config` on a shared filesystem can set `core.protectNTFS = false` and/or `core.protectHFS = false`, disabling the `.git` path-confusion checks that run during checkout, index creation, merge, and tree editing. On case-insensitive filesystems this lets crafted tree entries resolve to the `.git` directory and overwrite hooks or config.

## Root Cause

Repositories opened under `Trust::Reduced` install a section filter (`is_trusted`) so that security-relevant settings coming from an untrusted repository-local `.git/config` are ignored. The `Cache` struct stores this filter and applies it throughout `gix/src/config/cache/access.rs` for diff drivers, merge drivers, URL rewrites, scheme permissions, lock timeout, and checkout workers. `protect_options()` is the exception: it reads the path-protection booleans through `self.resolved.boolean(...)`, which delegates to `boolean_filter(key, |_| true)` and therefore accepts values from every section regardless of trust. As a result, an untrusted repository-local setting of `core.protectHFS`/`core.protectNTFS`/`gitoxide.core.protectWindows` overrides the safe defaults, defeating the trust boundary for exactly the controls that are meant to prevent `.git` path-confusion attacks.

```text
gix/src/config/cache/access.rs:300  Cache::protect_options() : vulnerable function
gix/src/config/cache/access.rs:306  self.resolved.boolean(gitoxide.core.protectWindows)  (unfiltered)
gix/src/config/cache/access.rs:310  self.resolved.boolean(core.protectHFS)              (unfiltered)
gix/src/config/cache/access.rs:314  self.resolved.boolean(core.protectNTFS)             (unfiltered)
gix-config/src/file/access/comfort.rs:89  boolean() delegates to boolean_filter(key, |_| true)
gix/src/config/mod.rs:61  is_trusted() : the filter that would reject untrusted repo-local sections
gix-validate/src/path.rs:98  component() : path validation that honors the resulting options
```

## Impact

An attacker who owns a repository on a filesystem shared with the victim (a multi-user development host, a shared or network-mounted checkout, or a CI workspace reused across jobs) edits that repository's `.git/config` to disable one or both of `core.protectHFS` and `core.protectNTFS`. When the victim opens the repository with a `gix`-based tool, the ownership mismatch yields `Trust::Reduced`, which should neutralize these untrusted settings, but `protect_options()` honors them anyway. The attacker also commits tree entries whose names are filesystem-specific encodings of `.git` (for example NTFS short names such as `GIT~1`, NTFS alternate-data-stream or trailing-dot/space variants, or HFS+ forms using ignorable code points). When the victim performs a checkout, index build, merge, or tree edit, path validation no longer rejects these names, so on a case-insensitive filesystem (Windows NTFS, macOS HFS+) they collapse to the real `.git` directory. This permits overwriting repository hooks and configuration, which plausibly leads to arbitrary command execution in the victim's context on the next Git operation that runs a hook. The confidentiality, integrity, and availability impact follows from control over the victim's `.git` directory and any resulting hook execution; hook execution was not freshly executed here and is characterized as the plausible consequence of the demonstrated path-protection bypass.
## Proof of Concept

The following unit test places the config cache in the exact state produced after a submodule or repository is initialized (only `url` copied to the local config) and shows that the attacker's path-protection override from the repository-local config is honored even though it should be filtered under reduced trust. The same `boolean()` vs `boolean_filter()` discrepancy is directly observable in source.

```rust
// 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
```

```text
filtered (trusted only) = None
unfiltered (vulnerable) = Some(Ok(false))
```

Source confirmation that `boolean()` performs no trust filtering:

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

## Attack Chain

1. **Exposure:** The attacker owns a repository on a filesystem shared with the victim; the victim opens it with a `gix`-based tool, and the ownership mismatch sets the repository's config sections to `Trust::Reduced` (`gix/src/open/options.rs:215` installs the `is_trusted` section filter).
2. **Control:** The attacker writes `core.protectNTFS = false` (and/or `core.protectHFS = false`) into the repository-local `.git/config`, plus tree entries whose names are filesystem-specific encodings of `.git`.
3. **Path:** During checkout, index build, merge, or tree edit, each call site invokes `protect_options()` (`gix/src/clone/checkout.rs:103`, `gix/src/repository/index.rs:206`, `gix/src/repository/merge.rs:153` and `:214`, `gix/src/object/tree/editor.rs:24`, `gix/src/config/cache/access.rs:362`).
4. **Guard:** The `is_trusted` filter (`gix/src/config/mod.rs:61`) would reject the untrusted repository-local section, but `protect_options()` reads via `self.resolved.boolean(...)` (`gix/src/config/cache/access.rs:306,310,314`), which delegates to `boolean_filter(key, |_| true)` and ignores the filter.
5. **Primitive:** `gix_validate::path::component()` (`gix-validate/src/path.rs:98`) receives `protect_hfs=false` and/or `protect_ntfs=false`, so the HFS+/NTFS `.git`-confusion checks are skipped. When only one protection is disabled, the combined-case fallback at `gix-validate/src/path.rs:137` does not run, and the remaining single-filesystem check does not cover the other filesystem's encodings.
6. **Result:** Crafted tree entries that encode `.git` pass validation and are written to disk; on a case-insensitive filesystem they resolve into the real `.git` directory, overwriting hooks and configuration and plausibly yielding code execution in the victim's context.

## Bypass Evidence

The protection under test is the reduced-trust section filter that is supposed to stop untrusted repository-local config from altering security settings. The strongest counter-argument is the case-insensitive `.git` fallback in `gix-validate/src/path.rs:137`, which rejects a plain `.git` match when both HFS+ and NTFS protections are off. That fallback is insufficient for

## Which versions are affected by GHSA-q54r-jf2p-rfr2?

2 open source packages ship an affected version, including gix.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `gix` | crates.io | From 0.88.0 | 0.90.0 |
| `gix` | crates.io | From 0.88.0 | 0.90.0 |

## How do I fix GHSA-q54r-jf2p-rfr2?

Upgrade gix to 0.90.0 or later.

## Is GHSA-q54r-jf2p-rfr2 being exploited?

There is no known exploitation of GHSA-q54r-jf2p-rfr2 in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-q54r-jf2p-rfr2 and fix it if it is affected.

What it is: GHSA-q54r-jf2p-rfr2 is a vulnerability in gix, published on 23 Oct 2026. 
Exploitation: There is no known exploitation of GHSA-q54r-jf2p-rfr2 in the wild and no public exploit code.

Affected packages and fixed versions:
- gix (crates.io): affected from 0.88.0; fixed in 0.90.0
- gix (crates.io): affected from 0.88.0; fixed in 0.90.0

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-q54r-jf2p-rfr2/ (Markdown: https://insights.sbomify.com/GHSA-q54r-jf2p-rfr2/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-q54r-jf2p-rfr2

---
From sbomify Insights: https://insights.sbomify.com/GHSA-q54r-jf2p-rfr2/
Summaries are generated and may contain mistakes; the official description above is the record.
