Not ratedosv:GHSA-q54r-jf2p-rfr2

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

    Published Sources: OSV

    Share

    What is affected, what to do

    The available sources name no fixed version. Check the vendor guidance in the references below.

    Not sure whether and how you are affected? Ask a question about this vulnerability

    Metrics

    —

    Priority

    No value is available for this vulnerability.

    —

    CVSS v3

    No value is available for this vulnerability.

    —

    EPSS score

    No value is available for this vulnerability.

    Show all metrics
    Severity
    Not rated
    Priority (0–325)

    The priority score is a consolidated metric that condenses several individual signals into a single, action-guiding ranking: technical severity (CVSS), exploitation likelihood (EPSS), known-exploited status (KEV) and the context of the affected asset. The aim is to answer the only truly important question in vulnerability management: what first?

    Read in glossary
    30.00 Methodology
    Exploit maturity

    An exploit is code or a technique that specifically uses a concrete vulnerability to force unintended behaviour — such as executing code or escalating privileges. It is the tool that turns a theoretical flaw into a practical attack.

    Read in glossary
    no public PoC known
    KEV status

    The KEV catalogue is a list maintained by the US authority CISA of vulnerabilities for which active exploitation in the wild has been confirmed. It answers not the question „how severe?“ but „is it already being attacked?“ — thereby sharply prioritising patching.

    Read in glossary
    —
    Published

    Classification under NIS2 and CRA

    Rule-based classification from exploitation status and severity. It does not replace a case-by-case assessment.

    NIS2

    NIS2 is EU Directive (EU) 2022/2555 to raise the level of cybersecurity across the Union. It substantially broadens obligations compared with the first NIS directive: more sectors, mandatory risk management, staged reporting duties and personal accountability of senior management.

    Read in glossary
    Handle the vulnerability in regular vulnerability handling under Art. 21(2)(e) NIS2.
    CRA

    The CRA is EU Regulation (EU) 2024/2847 setting horizontal cybersecurity requirements for „products with digital elements“ — from industrial controllers and software to connected consumer devices. For the first time manufacturers must ensure security across the entire product lifecycle, handle vulnerabilities and provide security updates.

    Read in glossary
    From 11.12.2027, Annex I Part II CRA requires manufacturers of affected products with digital elements to handle vulnerabilities without delay and provide security updates.
    ISO/IEC 27001

    ISO/IEC 27001 is the internationally leading standard for an information security management system (ISMS). It describes a risk-based, process-oriented framework to systematically plan, implement, review and improve information security.

    Read in glossary
    Control A.8.8, management of technical vulnerabilities: assess relevance to your systems, plan remediation and keep evidence.

    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.

    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.

    // 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)
    }
    

    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)
    • 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.

    Source: OSV (Open Source Vulnerabilities)