GHSA-q54r-jf2p-rfr2

    Dashboard / Vulnerabilities / GHSA-q54r-jf2p-rfr2

    GHSA-q54r-jf2p-rfr2

    Published: 23 Oct 2026Last Modified: 23 Oct 2026

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

    Details: ## 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 two reasons verified in source: it only runs when BOTH `protect_hfs` and `protect_ntfs` are false (`!(protect_hfs | protect_ntfs)` at line 137), so disabling a single protection leaves it inactive; and even when it runs, its exact `eq_ignore_ascii_case(b".git")` test does not cover filesystem-specific encodings such as NTFS short names (`GIT~1`), NTFS trailing-dot/space or alternate-data-stream forms, or HFS+ ignorable-code-point variants, which are precisely what the dedicated `is_dot_hfs`/`is_dot_git_ntfs` checks (now skipped) exist to catch. The `boolean()` accessor was confirmed to apply no trust filtering (`gix-config/src/file/access/comfort.rs:89`), and the sibling reads in the same file consistently apply `filter_config_section`, so the unfiltered read is a genuine deviation rather than an intentional exemption. This issue is distinct from the previously disclosed submodule command-execution bypass (CVE-2026-40034 / GHSA-f26g-jm89-4g65): that advisory's fix changed only the `gix-submodule` crate and `gix/src/repository/submodule.rs` and never touched `protect_options()`, so this code path and its remediation boundary are not covered by it. ## Affected Versions - **Ecosystem:** `rust` - **Package:** `gix` (crates.io) - **Confirmed affected range:** `>= 0.88.0, <= 0.89.0` - **Latest release checked:** `0.89.0` ([crates.io](https://crates.io/crates/gix/0.89.0)) - **Fix status:** `0.90.0` `protect_options()` was introduced in `gix` 0.88.0 and did not exist in earlier releases; before then the path-validation options were not configurable from repository config in this way. The unfiltered read is present from introduction through the latest published release (0.89.0). ## Suggested Fix Read the path-protection settings through the same trust-aware accessor used by the other security-critical reads in `gix/src/config/cache/access.rs`: replace each `self.resolved.boolean(...)` call in `protect_options()` with `self.resolved.boolean_filter(..., &mut self.filter_config_section.clone())` so that untrusted repository-local sections are excluded under `Trust::Reduced` and the safe defaults are retained. The fix must apply to all three keys (`core.protectHFS`, `core.protectNTFS`, `gitoxide.core.protectWindows`) so that no single protection can be disabled from untrusted config. As an interim mitigation, operators can avoid opening untrusted or other-user-owned repositories with `gix`-based tools on shared or case-insensitive filesystems; this does not remove the underlying bypass. --- Reported by zx.

    Affected packages

    Package

    Name: gix

    Purl: pkg:cargo/gix

    Affected ranges

    Type: SEMVER

    Events:

    Introduced- 0.88.0
    Fixed -0.90.0

    Affected versions

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High