GHSA-p3hw-mv63-rf9w — gix
GHSA-p3hw-mv63-rf9w is a security vulnerability in gix. A fix is available for gix — see the affected versions and patch details below.
gix's submodule name validation bypass + trust inheritance flaw enables path traversal and credential disclosure
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for GHSA-p3hw-mv63-rf9w.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
gix🦀gix-validateReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects crates.io packages — download data is not available via public APIs for these ecosystems.
Description
Summary
Submodule name validation bypass plus missing validation in production code paths allows path traversal via crafted .gitmodules. Combined with a trust inheritance flaw in Submodule::open(), this enables reading arbitrary git repository configs (including credentials) from traversed paths with full trust (CWE-22, CWE-200).
Details
Bug 1: Validation bypass in gix-validate/src/submodule.rs (lines 27-42)
The name() function uses name.find(b"..") which returns only the FIRST occurrence. If the first .. is embedded in a non-traversal context, the function returns Ok without checking subsequent ../ sequences:
pub fn name(name: &BStr) -> Result<&BStr, name::Error> {
match name.find(b"..") {
Some(pos) => {
let &b = name.get(pos + 2).ok_or(name::Error::ParentComponent)?;
if b == b'/' || b == b'\\' {
Err(name::Error::ParentComponent)
} else {
Ok(name) // Returns Ok without checking rest of string
}
}
None => Ok(name),
}
}
Bypass: a..b/../../../.git/ passes because find(b"..") returns position 1 (the .. in a..b), checks name[3] == b'b', and returns Ok. The real /../../../ is never checked.
Bug 2: Validation never called in production
gix_validate::submodule::name() has zero production callers (only test code). The names() iterator in gix-submodule/src/access.rs:29 explicitly documents it returns "unvalidated names."
git_dir() at gix/src/submodule/mod.rs:198-204 constructs filesystem paths from raw names:
pub fn git_dir(&self) -> PathBuf {
self.state.repo.common_dir().join("modules").join(gix_path::from_bstr(self.name()))
}
Bug 3: Trust inheritance bypass in Submodule::open()
At gix/src/submodule/mod.rs:270, open() clones the parent repository's options:
match crate::open_opts(self.git_dir_try_old_form()?, self.state.repo.options.clone()) {
The parent's options.git_dir_trust is Some(Trust::Full). At gix/src/open/repository.rs:103-104:
if options.git_dir_trust.is_none() {
options.git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir)?.into();
}
Since trust is already Some(Full), the ownership check is skipped entirely. The traversed path is opened with Trust::Full regardless of ownership, bypassing gitoxide's safe-directory protections.
PoC
Compiled and executed in Rust 1.94.1 --release mode. All bypass cases confirmed:
BYPASS a..b/../../../.git/ -> PASSED validation
git_dir = .git/modules/a..b/../../../.git/
normalized = .git/ (parent repo!)
BYPASS x..y/../../../.git/config -> PASSED validation
git_dir = .git/modules/x..y/../../../.git/config
normalized = .git/config
Attack chain
-
Attacker crafts a repository with
.gitmodules:[submodule "x..y/../../.."] path = innocent url = https://attacker.com/repo.git -
Victim clones the repository using a tool built on gitoxide.
-
When the tool iterates submodules and calls
submodule.open()orsubmodule.status():git_dir()returns.git/modules/x..y/../../..which resolves to the parent.git/open_opts()is called withTrust::Full(inherited from parent, ownership check skipped)- The parent's
.git/configis fully parsed
-
The returned
Repositoryobject exposes all config values from the traversed path:remote.origin.url(may containhttps://user:[email protected]/...)http.extraHeader(oftenAuthorization: Bearer <token>)credential.*sectionscore.sshCommand
-
Accessible via standard API:
repo.config_snapshot().string("http.extraHeader"),repo.find_remote("origin"), etc.
Impact
A crafted .gitmodules in a malicious repository causes gitoxide to open arbitrary git directories as submodule repositories with full trust, exposing their configuration including credentials. This is the same class of vulnerability as GHSA-7w47-3wg8-547c (path traversal), but through the submodule name vector with an additional trust bypass.
The trust inheritance is the critical amplifier: without it, the traversed path would undergo ownership checks that could block the attack. With it, any git directory reachable via ../ is opened with full trust.
Honest limitations
- The traversed path must be a valid git directory (HEAD, objects/, refs/ must exist)
- The victim's tool must call
open()orstatus()on submodules (tools that only list submodules are not affected) - Credential exposure requires the target config to contain embedded credentials
- Submodule operations currently require explicit user action
Suggested fix
- Fix the validation to check ALL
..occurrences (iterate, not singlefind) - Call
gix_validate::submodule::name()ingit_dir()before constructing the path - Do NOT inherit
git_dir_trustfrom parent when opening submodule repos -- always re-derive trust from path ownership
Severity
High. Network vector (via clone), requires user interaction (submodule operations). The trust bypass enables credential disclosure from traversed git directories. Confidentiality impact is high.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🦀crates.io | gix | all versions | 0.83.0cargo update -p gix --precise 0.83.0 |
| 🦀crates.io | gix-validate | all versions | 0.11.1cargo update -p gix-validate --precise 0.11.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for gix, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update gix to 0.83.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-p3hw-mv63-rf9w is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-p3hw-mv63-rf9w can be triaged on real exposure rather than presence alone.
Tailored to GHSA-p3hw-mv63-rf9w. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-p3hw-mv63-rf9w in your dependencies?
O3 Security finds GHSA-p3hw-mv63-rf9w across crates.io dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.