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"
}
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.