GHSA-P3HW-MV63-RF9W
Vulnerability from github – Published: 2026-05-05 19:20 – Updated: 2026-05-05 19:20Summary
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:ini [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:token@github.com/...)http.extraHeader(oftenAuthorization: Bearer <token>)credential.*sections-
core.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": [
{
"package": {
"ecosystem": "crates.io",
"name": "gix"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.83.0"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.10.0"
},
"package": {
"ecosystem": "crates.io",
"name": "gix-validate"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.11.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T19:20:38Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nSubmodule 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).\n\n### Details\n\n**Bug 1: Validation bypass in `gix-validate/src/submodule.rs` (lines 27-42)**\n\nThe `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:\n\n```rust\npub fn name(name: \u0026BStr) -\u003e Result\u003c\u0026BStr, name::Error\u003e {\n match name.find(b\"..\") {\n Some(pos) =\u003e {\n let \u0026b = name.get(pos + 2).ok_or(name::Error::ParentComponent)?;\n if b == b\u0027/\u0027 || b == b\u0027\\\\\u0027 {\n Err(name::Error::ParentComponent)\n } else {\n Ok(name) // Returns Ok without checking rest of string\n }\n }\n None =\u003e Ok(name),\n }\n}\n```\n\nBypass: `a..b/../../../.git/` passes because `find(b\"..\")` returns position 1 (the `..` in `a..b`), checks `name[3] == b\u0027b\u0027`, and returns Ok. The real `/../../../` is never checked.\n\n**Bug 2: Validation never called in production**\n\n`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.\"\n\n`git_dir()` at `gix/src/submodule/mod.rs:198-204` constructs filesystem paths from raw names:\n\n```rust\npub fn git_dir(\u0026self) -\u003e PathBuf {\n self.state.repo.common_dir().join(\"modules\").join(gix_path::from_bstr(self.name()))\n}\n```\n\n**Bug 3: Trust inheritance bypass in `Submodule::open()`**\n\nAt `gix/src/submodule/mod.rs:270`, `open()` clones the parent repository\u0027s options:\n\n```rust\nmatch crate::open_opts(self.git_dir_try_old_form()?, self.state.repo.options.clone()) {\n```\n\nThe parent\u0027s `options.git_dir_trust` is `Some(Trust::Full)`. At `gix/src/open/repository.rs:103-104`:\n\n```rust\nif options.git_dir_trust.is_none() {\n options.git_dir_trust = gix_sec::Trust::from_path_ownership(\u0026git_dir)?.into();\n}\n```\n\nSince trust is already `Some(Full)`, the ownership check is **skipped entirely**. The traversed path is opened with `Trust::Full` regardless of ownership, bypassing gitoxide\u0027s safe-directory protections.\n\n### PoC\n\nCompiled and executed in Rust 1.94.1 `--release` mode. All bypass cases confirmed:\n\n```\nBYPASS a..b/../../../.git/ -\u003e PASSED validation\n git_dir = .git/modules/a..b/../../../.git/\n normalized = .git/ (parent repo!)\n\nBYPASS x..y/../../../.git/config -\u003e PASSED validation\n git_dir = .git/modules/x..y/../../../.git/config\n normalized = .git/config\n```\n\n### Attack chain\n\n1. Attacker crafts a repository with `.gitmodules`:\n ```ini\n [submodule \"x..y/../../..\"]\n path = innocent\n url = https://attacker.com/repo.git\n ```\n\n2. Victim clones the repository using a tool built on gitoxide.\n\n3. When the tool iterates submodules and calls `submodule.open()` or `submodule.status()`:\n - `git_dir()` returns `.git/modules/x..y/../../..` which resolves to the parent `.git/`\n - `open_opts()` is called with `Trust::Full` (inherited from parent, ownership check skipped)\n - The parent\u0027s `.git/config` is fully parsed\n\n4. The returned `Repository` object exposes all config values from the traversed path:\n - `remote.origin.url` (may contain `https://user:token@github.com/...`)\n - `http.extraHeader` (often `Authorization: Bearer \u003ctoken\u003e`)\n - `credential.*` sections\n - `core.sshCommand`\n\n5. Accessible via standard API: `repo.config_snapshot().string(\"http.extraHeader\")`, `repo.find_remote(\"origin\")`, etc.\n\n### Impact\n\nA 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.\n\nThe 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.\n\n### Honest limitations\n\n- The traversed path must be a valid git directory (HEAD, objects/, refs/ must exist)\n- The victim\u0027s tool must call `open()` or `status()` on submodules (tools that only list submodules are not affected)\n- Credential exposure requires the target config to contain embedded credentials\n- Submodule operations currently require explicit user action\n\n### Suggested fix\n\n1. Fix the validation to check ALL `..` occurrences (iterate, not single `find`)\n2. Call `gix_validate::submodule::name()` in `git_dir()` before constructing the path\n3. Do NOT inherit `git_dir_trust` from parent when opening submodule repos -- always re-derive trust from path ownership\n\n### Severity\n\nHigh. Network vector (via clone), requires user interaction (submodule operations). The trust bypass enables credential disclosure from traversed git directories. Confidentiality impact is high.",
"id": "GHSA-p3hw-mv63-rf9w",
"modified": "2026-05-05T19:20:38Z",
"published": "2026-05-05T19:20:38Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/GitoxideLabs/gitoxide/security/advisories/GHSA-p3hw-mv63-rf9w"
},
{
"type": "PACKAGE",
"url": "https://github.com/GitoxideLabs/gitoxide"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "gix\u0027s submodule name validation bypass + trust inheritance flaw enables path traversal and credential disclosure"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.