Live › vulnerability › GHSA-q54r-jf2p-rfr2
Go to dashboardThe gix Rust crate ignores repository trust when reading path-protection settings, so an untrusted .git/config can turn off the checks that reject .git look-alike path names. An attacker who controls a repository on a shared case-insensitive filesystem can use crafted tree entries to overwrite hooks and config in the victim's .git directory, which the advisory says plausibly leads to code execution.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-23 20:59 UTC. All times on this page are UTC.
| Product | Ecosystem | Affected | Fixed in |
|---|
| gix | cargo | >= 0.88.0, <= 0.89.0 | 0.90.0 |
0.88.0 and 0.89.00.90.0 or later (cargo update -p gix)Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
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.
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.
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
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.
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.
// 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
filtered (trusted only) = None
unfiltered (vulnerable) = Some(Ok(false))
Source confirmation that boolean() performs no trust filtering:
// gix-config/src/file/access/comfort.rs:89
pub fn boolean(&self, key: impl AsKey) -> Result<Option<bool>> {
self.boolean_filter(key, |_| true)
}
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).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.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).is_trusted filter () would reject the untrusted repository-local section, but reads via (), which delegates to and ignores the filter.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
This page as .md · .json · in your agent: get_signal("GHSA-q54r-jf2p-rfr2")
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.
gix/src/config/mod.rs:61protect_options()self.resolved.boolean(...)gix/src/config/cache/access.rs:306,310,314boolean_filter(key, |_| true)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..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.