gitoxide gix: untrusted .git/config disables filesystem .git-protection path checks via unfiltered protect_options()
GHSA-q54r-jf2p-rfr2 · published 23 Oct 2026
How serious is it
- Severity: high, CVSS score 7.3 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by GHSA-q54r-jf2p-rfr2?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
gix(crates.io) >= 0.88.0, <= 0.89.0
How to check
- Run
cargo tree -i gixin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
gixto 0.90.0 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## 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 reposit…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/GitoxideLabs/gitoxide/security/advisories/GHSA-q54r-jf2p-rfr2
- github.com/GitoxideLabs/gitoxide/pull/3068
- github.com/GitoxideLabs/gitoxide/commit/743957880154aa1726b30b8b5779fd10c78a058c
- github.com/GitoxideLabs/gitoxide/releases/tag/gix-v0.90.0
- github.com/advisories/GHSA-q54r-jf2p-rfr2
- github.com/GitoxideLabs/gitoxide
Merged from GitHub Advisory Database, OSV. Last updated 26 Oct 2026. All vulnerabilities · RSS · JSON